티스토리 뷰

반응형

📌 시리즈 안내 — 7일 완성 온톨로지 마스터 과정

일차 주제 핵심 내용
1일차 온톨로지 기초 온톨로지란? 왜 필요한가? 3대 구성요소
2일차 구축 프레임워크 5단계 프로세스, 범위 정의, 용어 수집 방법론
3일차 실전 설계 설비관리·불량분석 온톨로지 전체 설계 실습
4일차 (오늘) 구현 실습 엑셀 온톨로지 완성, JSON-LD 변환, Python 코드
5일차 AI 연동 프롬프트 엔지니어링 × 온톨로지, RAG 연동
6일차 실전 시나리오 불량 자동분석, 사내 AI 챗봇 구축 사례
7일차 30일 로드맵 추천 도구, 기술 스택, 주차별 실행 계획

💡 3일차 복습: 설비 관리·불량 분석 온톨로지를 "목표 질문 → 클래스 → 관계 → 규칙 → 추론 경로 검증" 순서로
설계했습니다. 오늘은 그 설계를 실제 파일로 만듭니다. → 3일차 다시 보기


목차

  1. 오늘의 목표: 동작하는 온톨로지 만들기
  2. 방법 ①: 엑셀 3시트 온톨로지 (비개발자용)
  3. 방법 ②: JSON-LD로 변환하기 (개발자용)
  4. 인스턴스 입력 실습 — 실제 데이터 채워넣기
  5. Python으로 온톨로지 자동 생성하기
  6. 엑셀 → JSON-LD 자동 변환 코드
  7. 구현 시 자주 발생하는 오류 7가지
  8. 4일차 핵심 정리 & 5일차 예고

1. 오늘의 목표: 동작하는 온톨로지 만들기

3일차까지는 "설계"였습니다. 종이 위에 구조를 그렸지만, 아직 AI가 읽을 수 있는 형태는 아닙니다.

오늘은 그 설계를 실제 파일로 만듭니다. 기술 역량에 따라 두 가지 경로가 있습니다.

경로 A (비개발자): 엑셀 3시트 → 바로 AI 프롬프트에 활용
경로 B (개발자):   엑셀 3시트 → Python으로 JSON-LD 변환 → RAG/시스템 연동

두 경로 모두 시작점은 같은 엑셀입니다. 엑셀을 먼저 완성하고, 개발 역량이 있다면 JSON-LD로 확장하는 구조입니다.

오늘이 끝나면 여러분 손에 있어야 할 파일:

✅ 파일 1: ontology_classes.xlsx — 클래스 + 관계 + 인스턴스 (엑셀)
✅ 파일 2: ontology.json — JSON-LD 형식 온톨로지 (선택)
✅ 파일 3: excel_to_jsonld.py — 자동 변환 스크립트 (선택)

2. 방법 ①: 엑셀 3시트 온톨로지 (비개발자용)

프로그래밍 없이 엑셀만으로 완전한 온톨로지를 구현할 수 있습니다. 핵심은 3개 시트의 구조를 정확히 지키는 것입니다.

시트 1: 클래스 정의 (Classes)

클래스의 계층 구조와 기본 정보를 정의합니다.

class_id class_name_kr class_name_en parent_class definition synonyms level
C001 설비 Equipment (root) 생산에 사용되는 기계장치 전체 장비, 기계 1
C002 가공설비 Processing Equipment C001 소재를 가공하는 설비 가공기 2
C003 CNC 선반 CNC Lathe C002 컴퓨터 수치제어 선반 가공 설비 CNC, 씨엔씨, 선반 3
C004 CNC 밀링 CNC Milling C002 컴퓨터 수치제어 밀링 가공 설비 밀링, MCT 3
C005 프레스 Press C002 금속 성형 프레스 설비 프레스기 3
C006 사출기 Injection Molding C002 플라스틱 사출 성형 설비 사출성형기 3
C007 검사설비 Inspection Equipment C001 제품 품질 검사에 사용되는 설비 측정장비 2
C008 CMM CMM C007 3차원 좌표 측정기 삼차원측정기 3
C009 비전검사기 Vision Inspector C007 카메라 기반 자동 외관 검사 설비 비전, 영상검사기 3
C010 고장 Failure (root) 설비 정상 가동을 방해하는 이상 상태 이상, 에러, 장애 1
C011 기계적고장 Mechanical Failure C010 물리적 원인에 의한 고장 기계고장 2
C012 스핀들과열 Spindle Overheat C011 주축 온도 80℃ 초과 상태 스핀들빨간불, 주축과열 3
C013 진동이상 Abnormal Vibration C011 진동값 정상범위 초과 상태 진동, 떨림 3
C014 전기적고장 Electrical Failure C010 전기·전자 원인에 의한 고장 전기고장 2
C015 센서오류 Sensor Error C014 센서 측정값 이상 또는 무응답 센서불량 3
C016 부품 Part (root) 설비를 구성하는 개별 구성요소 파트, 자재 1
C017 소모품 Consumable C016 정기적 교체가 필요한 부품 - 2
C018 절삭공구 Cutting Tool C017 소재를 절삭하는 공구 인서트, 바이트, 공구 3
C019 핵심부품 Critical Part C016 설비 핵심 기능에 관여하는 부품 주요부품 2
C020 볼스크류 Ball Screw C019 이송축 구동 정밀 부품 BS, 볼스크류 3
C021 베어링 Bearing C019 회전축 지지 부품 - 3
C022 위치 Location (root) 설비가 배치된 물리적 장소 장소 1
C023 담당자 Person (root) 설비 관련 업무 담당자 작업자, 인력 1
C024 센서데이터 Sensor Data (root) 설비에 부착된 센서의 측정값 센서값, 모니터링 1

작성 규칙:

1. class_id는 "C" + 3자리 숫자로 통일 (C001, C002...)
2. parent_class가 "(root)"이면 최상위 클래스
3. synonyms에 현장 용어를 최대한 많이 넣기 (쉼표 구분)
4. level은 계층 깊이 (1=최상위, 2=중간, 3=최하위)
5. definition은 신입사원도 이해할 수 있게 쉽게 작성

시트 2: 관계 정의 (Relations)

클래스 간의 관계를 정의합니다. 두 종류로 나눕니다.

2-A. 객체 관계 (Object Properties)

rel_id relation_kr relation_en domain_class range_class inverse_relation description
R001 ~에 위치한다 located_in C001(설비) C022(위치) has_equipment 설비의 물리적 위치
R002 ~를 담당한다 maintained_by C001(설비) C023(담당자) manages 설비 유지보수 담당자
R003 ~고장이 발생 has_failure C001(설비) C010(고장) occurred_at 해당 설비의 고장 이력
R004 ~가 원인 caused_by C010(고장) (원인텍스트) causes 고장의 근본 원인
R005 ~부품이 필요 requires_part C001(설비) C016(부품) used_in 설비 구성 부품
R006 ~로 해결 resolved_by C010(고장) (조치텍스트) resolves 고장 해결 방법
R007 ~센서 연결 has_sensor C001(설비) C024(센서) monitors 설비 모니터링 센서
R008 ~관련 부품 related_part C010(고장) C016(부품) failure_risk 고장과 관련된 부품

2-B. 데이터 속성 (Data Properties)

prop_id property_kr property_en target_class data_type unit example
P001 도입일 install_date C001(설비) 날짜 - 2020-03-15
P002 누적가동시간 total_hours C001(설비) 숫자 시간 12500
P003 상태 status C001(설비) 선택 - 가동/정지/점검
P004 발생일시 occurred_at C010(고장) 날짜시간 - 2024-12-01 14:30
P005 심각도 severity C010(고장) 선택 - 경미/보통/심각
P006 교체주기 replacement_cycle C016(부품) 숫자 365
P007 최종교체일 last_replaced C016(부품) 날짜 - 2024-06-15
P008 현재값 current_value C024(센서) 숫자 (가변) 78.5
P009 정상범위상한 upper_limit C024(센서) 숫자 (가변) 80.0
P010 정상범위하한 lower_limit C024(센서) 숫자 (가변) 20.0

시트 3: 인스턴스 (Instances)

실제 데이터를 채워넣습니다. 이 시트가 AI가 활용할 구체적인 지식입니다.

inst_id name class_id class_name prop:intall_date prop:total_hours prop:status rel:located_in rel:maintained_by rel:requires_part
I001 CNC 1호기 C003 CNC 선반 2020-03-15 12500 가동중 1공장 A라인 김기술 볼스크류, 베어링, 절삭공구
I002 CNC 2호기 C003 CNC 선반 2021-06-01 8300 가동중 1공장 B라인 이설비 볼스크류, 베어링, 절삭공구
I003 CNC 3호기 C004 CNC 밀링 2019-01-10 18200 점검중 2공장 A라인 김기술 볼스크류, 스핀들, 절삭공구
I004 200톤프레스 C005 프레스 2018-05-20 22100 가동중 2공장 B라인 박정비 금형, 유압실린더
I005 스핀들과열_240301 C012 스핀들과열 - - - CNC 1호기 - 스핀들, 베어링
I006 진동이상_240515 C013 진동이상 - - - CNC 3호기 - 볼스크류

작성 팁:

1. 설비 인스턴스를 먼저 채우고, 그 다음 고장 이력을 채운다
2. 고장 인스턴스의 inst_id는 "유형_날짜" 형식으로 관리가 쉽다
3. 관계(rel:) 열에는 연결 대상의 이름을 직접 넣는다
4. 처음에는 설비 5~10대 + 고장 이력 10~20건이면 충분하다
5. 나중에 MES/ERP에서 자동 추출로 전환할 수 있다

💡 이 엑셀 하나면 AI 연동이 가능합니다.
5일차에서 다룰 프롬프트 엔지니어링 방식은 이 엑셀 내용을 그대로 AI 시스템 메시지에 넣는 것입니다.
코딩 없이도 "우리 회사 맞춤 AI"를 만들 수 있습니다.


3. 방법 ②: JSON-LD로 변환하기 (개발자용)

엑셀은 사람이 읽기 편하지만, 시스템이 처리하기에는 JSON-LD가 훨씬 효율적입니다. JSON-LD는 웹 표준(W3C) 기반의 온톨로지 표현 형식으로, 대부분의 프로그래밍 언어에서 쉽게 다룰 수 있습니다.

JSON-LD 기본 구조 이해

JSON-LD는 일반 JSON에 **의미(Semantic)**를 추가한 형식입니다.

{
  "@context": {
    "equip": "http://yourcompany.com/ontology/equipment#",
    "fail": "http://yourcompany.com/ontology/failure#",
    "part": "http://yourcompany.com/ontology/part#"
  },
  "@graph": [
    {
      "@id": "equip:CNC_1",
      "@type": "equip:CNC_Lathe",
      "equip:name": "CNC 1호기",
      "equip:install_date": "2020-03-15",
      "equip:total_hours": 12500,
      "equip:status": "가동중",
      "equip:located_in": "1공장 A라인",
      "equip:maintained_by": "김기술",
      "equip:requires_part": [
        {"@id": "part:BallScrew_CNC1"},
        {"@id": "part:Bearing_CNC1"},
        {"@id": "part:CuttingTool_CNC1"}
      ],
      "equip:has_failure": [
        {"@id": "fail:SpindleOverheat_240301"}
      ]
    }
  ]
}

핵심 요소 해설:

@context  → 약속된 용어의 URL (네임스페이스 정의)
@graph    → 실제 데이터 목록
@id       → 각 개체의 고유 식별자
@type     → 이 개체가 속하는 클래스
나머지 키  → 속성(Property)과 관계(Relation)

설비 온톨로지 JSON-LD 전체 예시

3일차에서 설계한 설비 관리 온톨로지의 전체 JSON-LD 구현입니다.

{
  "@context": {
    "equip": "http://yourcompany.com/ontology/equipment#",
    "fail": "http://yourcompany.com/ontology/failure#",
    "part": "http://yourcompany.com/ontology/part#",
    "sensor": "http://yourcompany.com/ontology/sensor#",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
    "xsd": "http://www.w3.org/2001/XMLSchema#"
  },
  "@graph": [

    {"@id": "equip:Equipment", "@type": "rdfs:Class",
     "rdfs:label": "설비",
     "rdfs:comment": "생산에 사용되는 기계장치 전체",
     "equip:synonyms": ["장비", "기계", "Equipment"]},

    {"@id": "equip:ProcessingEquipment", "@type": "rdfs:Class",
     "rdfs:subClassOf": {"@id": "equip:Equipment"},
     "rdfs:label": "가공설비"},

    {"@id": "equip:CNC_Lathe", "@type": "rdfs:Class",
     "rdfs:subClassOf": {"@id": "equip:ProcessingEquipment"},
     "rdfs:label": "CNC 선반",
     "equip:synonyms": ["CNC", "씨엔씨", "선반"]},

    {"@id": "fail:Failure", "@type": "rdfs:Class",
     "rdfs:label": "고장",
     "equip:synonyms": ["이상", "에러", "장애"]},

    {"@id": "fail:MechanicalFailure", "@type": "rdfs:Class",
     "rdfs:subClassOf": {"@id": "fail:Failure"},
     "rdfs:label": "기계적고장"},

    {"@id": "fail:SpindleOverheat", "@type": "rdfs:Class",
     "rdfs:subClassOf": {"@id": "fail:MechanicalFailure"},
     "rdfs:label": "스핀들과열",
     "equip:synonyms": ["스핀들빨간불", "주축과열"],
     "fail:diagnostic_rule": {
       "check_order": [
         {"priority": 1, "check": "윤활유 잔량", "cause": "윤활부족", "probability": 0.4},
         {"priority": 2, "check": "베어링 상태", "cause": "베어링마모", "probability": 0.3},
         {"priority": 3, "check": "가공 부하", "cause": "과부하운전", "probability": 0.2},
         {"priority": 4, "check": "냉각 시스템", "cause": "냉각불량", "probability": 0.1}
       ]
     }},

    {"@id": "fail:AbnormalVibration", "@type": "rdfs:Class",
     "rdfs:subClassOf": {"@id": "fail:MechanicalFailure"},
     "rdfs:label": "진동이상",
     "equip:synonyms": ["진동", "떨림"],
     "fail:diagnostic_rule": {
       "check_order": [
         {"priority": 1, "check": "볼스크류 마모", "cause": "볼스크류마모", "probability": 0.35},
         {"priority": 2, "check": "베어링 손상", "cause": "베어링손상", "probability": 0.3},
         {"priority": 3, "check": "체결 상태", "cause": "체결불량", "probability": 0.2},
         {"priority": 4, "check": "기초 상태", "cause": "기초불량", "probability": 0.15}
       ]
     }},

    {"@id": "equip:CNC_1", "@type": "equip:CNC_Lathe",
     "equip:name": "CNC 1호기",
     "equip:install_date": {"@value": "2020-03-15", "@type": "xsd:date"},
     "equip:total_hours": 12500,
     "equip:status": "가동중",
     "equip:located_in": "1공장 A라인",
     "equip:maintained_by": "김기술",
     "equip:requires_part": [
       {"@id": "part:BallScrew_CNC1", "part:name": "볼스크류",
        "part:replacement_cycle_days": 365, "part:last_replaced": "2024-06-15"},
       {"@id": "part:Bearing_CNC1", "part:name": "베어링",
        "part:replacement_cycle_days": 730, "part:last_replaced": "2023-12-01"},
       {"@id": "part:CuttingTool_CNC1", "part:name": "절삭공구",
        "part:replacement_cycle_days": 30, "part:last_replaced": "2024-11-20"}
     ],
     "equip:has_sensor": [
       {"@id": "sensor:Temp_CNC1", "sensor:type": "온도",
        "sensor:current_value": 72.3, "sensor:upper_limit": 80.0, "sensor:lower_limit": 20.0},
       {"@id": "sensor:Vib_CNC1", "sensor:type": "진동",
        "sensor:current_value": 3.2, "sensor:upper_limit": 4.5, "sensor:lower_limit": 0.5}
     ],
     "equip:has_failure": [
       {"@id": "fail:SpindleOverheat_240301",
        "@type": "fail:SpindleOverheat",
        "fail:occurred_at": "2024-03-01T14:30:00",
        "fail:severity": "심각",
        "fail:caused_by": "윤활유부족",
        "fail:resolved_by": "윤활유 보충 및 베어링 점검",
        "fail:resolution_hours": 4.5}
     ]}
  ]
}

이 JSON-LD를 보면서 3일차 설계와 비교해보세요.

3일차 설계           →  4일차 구현(JSON-LD)
──────────────────────────────────────────
클래스 계층 구조     →  @type + rdfs:subClassOf
관계(속성)          →  equip:has_failure, equip:located_in 등
데이터 속성         →  equip:total_hours, fail:severity 등
추론 규칙           →  fail:diagnostic_rule 내 check_order
인스턴스            →  equip:CNC_1 등 실제 데이터
동의어              →  equip:synonyms 배열

설계 → 구현은 1:1 매핑입니다. 설계가 정확하면 구현은 기계적으로 채워넣는 작업입니다.


4. 인스턴스 입력 실습 — 실제 데이터 채워넣기

온톨로지의 가치는 인스턴스의 양과 정확성에서 나옵니다. 구조(클래스, 관계)가 뼈대라면, 인스턴스는 살과 근육입니다.

인스턴스 입력 우선순위

한꺼번에 다 넣으려 하지 마세요. 우선순위를 정해 단계적으로 채웁니다.

[1차 입력 - 즉시 (1~2일)]
  설비 인스턴스: 주요 설비 10~20대
  → MES 설비 마스터 또는 설비대장에서 추출

[2차 입력 - 1주 이내]
  고장 이력 인스턴스: 최근 6개월 주요 고장 20~30건
  → MES 알람 이력 또는 설비 보전 일지에서 추출

[3차 입력 - 2주 이내]
  부품 인스턴스: 주요 설비별 핵심 부품 + 교체 이력
  → 구매 이력 또는 설비 보전 기록에서 추출

[4차 입력 - 지속적]
  센서 데이터: 실시간 연동 (MES API 또는 수동 입력)
  → 장기적으로 자동화 목표

 

입력 소스별 추출 방법

소스 추출 방법 추출 대상 난이도
ERP 설비 마스터 엑셀 다운로드 설비명, 도입일, 위치 ★☆☆
MES 알람 이력 데이터 조회 화면 고장 유형, 발생일시, 설비 ★☆☆
설비 보전 일지 수기 문서 스캔/입력 부품 교체 이력, 원인 분석 ★★☆
센서 데이터 API 또는 CSV 내보내기 온도, 진동, 전류 실시간값 ★★★
품질 보고서 PDF/엑셀 분석 불량 유형, 원인, 조치내용 ★★☆

💡 가장 빠른 방법:
ERP/MES에서 엑셀로 다운로드 → 열 이름을 온톨로지 속성명에 맞춰 변경 → 시트 3에 복사·붙여넣기.
데이터 정제가 필요하지만, 바닥부터 수기 입력하는 것보다 10배 빠릅니다.

인스턴스 입력 시 필수 규칙

규칙 1: 하나의 개체에는 반드시 하나의 class_id
  ✅ CNC 1호기 → class_id: C003 (CNC 선반)
  ❌ CNC 1호기 → class_id: C002, C003 (중복 불가)

규칙 2: 관계는 실존하는 인스턴스를 가리켜야 한다
  ✅ CNC 1호기 → maintained_by: 김기술 (김기술이 담당자로 등록됨)
  ❌ CNC 1호기 → maintained_by: 누군가 (등록되지 않은 대상)

규칙 3: 날짜 형식을 통일한다
  ✅ 2024-03-15 (ISO 8601 형식)
  ❌ 24.3.15, 2024/03/15, 3월 15일 (혼재 금지)

규칙 4: 선택형 속성은 사전에 정의된 값만 사용한다
  ✅ status: "가동중" | "정지" | "점검중"
  ❌ status: "돌아가는 중", "멈춤", "수리" (표준화 안 된 표현)

5. Python으로 온톨로지 자동 생성하기

수작업 입력에 한계를 느꼈다면, Python으로 자동화할 수 있습니다. MES/ERP에서 추출한 엑셀을 읽어서 JSON-LD 온톨로지를 자동 생성하는 코드입니다.

기본 환경 설정

# 필요 라이브러리 설치
# pip install openpyxl

import json
from datetime import datetime, timedelta

온톨로지 클래스 정의 코드

def create_ontology_context():
    """온톨로지 네임스페이스(Context) 생성"""
    return {
        "equip": "http://yourcompany.com/ontology/equipment#",
        "fail": "http://yourcompany.com/ontology/failure#",
        "part": "http://yourcompany.com/ontology/part#",
        "sensor": "http://yourcompany.com/ontology/sensor#",
        "rdfs": "http://www.w3.org/2000/01/rdf-schema#"
    }

def create_class_definition(class_id, label, parent=None, synonyms=None, comment=None):
    """클래스 정의 생성"""
    cls = {
        "@id": class_id,
        "@type": "rdfs:Class",
        "rdfs:label": label
    }
    if parent:
        cls["rdfs:subClassOf"] = {"@id": parent}
    if synonyms:
        cls["equip:synonyms"] = synonyms
    if comment:
        cls["rdfs:comment"] = comment
    return cls

def create_equipment_instance(equip_id, name, equip_type, **properties):
    """설비 인스턴스 생성"""
    instance = {
        "@id": f"equip:{equip_id}",
        "@type": equip_type,
        "equip:name": name
    }
    # 동적으로 속성 추가
    for key, value in properties.items():
        if value is not None and value != "":
            instance[f"equip:{key}"] = value
    return instance

def create_failure_instance(fail_id, fail_type, equipment_id, **properties):
    """고장 이력 인스턴스 생성"""
    instance = {
        "@id": f"fail:{fail_id}",
        "@type": fail_type,
        "fail:equipment": {"@id": f"equip:{equipment_id}"}
    }
    for key, value in properties.items():
        if value is not None and value != "":
            instance[f"fail:{key}"] = value
    return instance

추론 규칙 엔진 코드

온톨로지의 규칙을 실제로 실행할 수 있는 간단한 추론 엔진입니다.

class OntologyReasoner:
    """간단한 온톨로지 추론 엔진"""
    
    def __init__(self, ontology_data):
        self.data = ontology_data
        self.graph = ontology_data.get("@graph", [])
    
    def find_by_type(self, type_id):
        """특정 타입의 모든 인스턴스 검색"""
        return [item for item in self.graph 
                if item.get("@type") == type_id]
    
    def find_by_id(self, item_id):
        """ID로 특정 인스턴스 검색"""
        for item in self.graph:
            if item.get("@id") == item_id:
                return item
        return None
    
    def check_part_replacement(self, today=None):
        """규칙 R-EQ-01: 부품 교체 시기 확인"""
        if today is None:
            today = datetime.now()
        
        alerts = []
        # 모든 설비 인스턴스 순회
        for item in self.graph:
            if "equip:requires_part" in item:
                equip_name = item.get("equip:name", "")
                parts = item["equip:requires_part"]
                if not isinstance(parts, list):
                    parts = [parts]
                
                for p in parts:
                    cycle = p.get("part:replacement_cycle_days", 0)
                    last = p.get("part:last_replaced", "")
                    if cycle and last:
                        last_date = datetime.strptime(last, "%Y-%m-%d")
                        due_date = last_date + timedelta(days=cycle)
                        if today > due_date:
                            overdue = (today - due_date).days
                            alerts.append({
                                "equipment": equip_name,
                                "part": p.get("part:name", ""),
                                "overdue_days": overdue,
                                "last_replaced": last,
                                "message": f"{equip_name}의 {p.get('part:name', '')} 교체주기 {overdue}일 초과"
                            })
        return alerts
    
    def check_sensor_anomaly(self):
        """규칙 R-EQ-02: 센서 이상 감지"""
        alerts = []
        for item in self.graph:
            if "equip:has_sensor" in item:
                equip_name = item.get("equip:name", "")
                sensors = item["equip:has_sensor"]
                if not isinstance(sensors, list):
                    sensors = [sensors]
                
                for s in sensors:
                    current = s.get("sensor:current_value", 0)
                    upper = s.get("sensor:upper_limit", float('inf'))
                    lower = s.get("sensor:lower_limit", float('-inf'))
                    
                    if current > upper or current < lower:
                        alerts.append({
                            "equipment": equip_name,
                            "sensor_type": s.get("sensor:type", ""),
                            "current_value": current,
                            "normal_range": f"{lower} ~ {upper}",
                            "message": f"{equip_name} {s.get('sensor:type', '')} 이상: {current} (정상: {lower}~{upper})"
                        })
        return alerts
    
    def diagnose_failure(self, failure_type):
        """규칙 R-EQ-03/04: 고장 유형별 원인 추론"""
        for item in self.graph:
            if item.get("rdfs:label") == failure_type:
                rule = item.get("fail:diagnostic_rule", {})
                return rule.get("check_order", [])
        return []

# 사용 예시
# reasoner = OntologyReasoner(ontology_json)
# print(reasoner.check_part_replacement())
# print(reasoner.diagnose_failure("스핀들과열"))

이 추론 엔진을 사용하면 다음과 같은 질문에 프로그래밍으로 답할 수 있습니다:

# "교체 시기가 지난 부품이 있는 설비는?"
alerts = reasoner.check_part_replacement()
for a in alerts:
    print(a["message"])
# 출력: CNC 1호기의 볼스크류 교체주기 45일 초과

# "센서 이상이 있는 설비는?"
anomalies = reasoner.check_sensor_anomaly()
for a in anomalies:
    print(a["message"])
# 출력: CNC 3호기 진동 이상: 5.1 (정상: 0.5~4.5)

# "스핀들 과열이면 뭘 확인해야 해?"
steps = reasoner.diagnose_failure("스핀들과열")
for s in steps:
    print(f"{s['priority']}순위: {s['check']} ({s['cause']}, 확률 {s['probability']*100}%)")
# 출력:
# 1순위: 윤활유 잔량 (윤활부족, 확률 40%)
# 2순위: 베어링 상태 (베어링마모, 확률 30%)
# ...

6. 엑셀 → JSON-LD 자동 변환 코드

엑셀 3시트 온톨로지를 JSON-LD로 자동 변환하는 스크립트입니다. 엑셀만 관리하면 JSON-LD가 자동으로 생성됩니다.

import openpyxl
import json

def excel_to_jsonld(excel_path, output_path):
    """엑셀 온톨로지를 JSON-LD로 변환"""
    
    wb = openpyxl.load_workbook(excel_path)
    graph = []
    
    # ── 시트 1: 클래스 변환 ──
    ws_classes = wb["Classes"]
    headers = [cell.value for cell in ws_classes[1]]
    
    for row in ws_classes.iter_rows(min_row=2, values_only=True):
        row_dict = dict(zip(headers, row))
        if not row_dict.get("class_id"):
            continue
        
        cls = {
            "@id": f"onto:{row_dict['class_id']}",
            "@type": "rdfs:Class",
            "rdfs:label": row_dict.get("class_name_kr", ""),
            "rdfs:comment": row_dict.get("definition", "")
        }
        
        parent = row_dict.get("parent_class", "")
        if parent and parent != "(root)":
            cls["rdfs:subClassOf"] = {"@id": f"onto:{parent}"}
        
        synonyms = row_dict.get("synonyms", "")
        if synonyms:
            cls["onto:synonyms"] = [s.strip() for s in synonyms.split(",")]
        
        graph.append(cls)
    
    # ── 시트 2: 관계 변환 ──
    if "Relations" in wb.sheetnames:
        ws_rels = wb["Relations"]
        headers = [cell.value for cell in ws_rels[1]]
        
        for row in ws_rels.iter_rows(min_row=2, values_only=True):
            row_dict = dict(zip(headers, row))
            if not row_dict.get("rel_id"):
                continue
            
            rel = {
                "@id": f"onto:{row_dict['rel_id']}",
                "@type": "rdf:Property",
                "rdfs:label": row_dict.get("relation_kr", ""),
                "rdfs:domain": row_dict.get("domain_class", ""),
                "rdfs:range": row_dict.get("range_class", "")
            }
            graph.append(rel)
    
    # ── 시트 3: 인스턴스 변환 ──
    ws_inst = wb["Instances"]
    headers = [cell.value for cell in ws_inst[1]]
    
    for row in ws_inst.iter_rows(min_row=2, values_only=True):
        row_dict = dict(zip(headers, row))
        if not row_dict.get("inst_id"):
            continue
        
        instance = {
            "@id": f"onto:{row_dict['inst_id']}",
            "@type": f"onto:{row_dict.get('class_id', '')}",
            "onto:name": row_dict.get("name", "")
        }
        
        # prop: 접두사 속성 처리
        for key, value in row_dict.items():
            if key and value:
                if key.startswith("prop:"):
                    prop_name = key.replace("prop:", "")
                    instance[f"onto:{prop_name}"] = value
                elif key.startswith("rel:"):
                    rel_name = key.replace("rel:", "")
                    instance[f"onto:{rel_name}"] = value
        
        graph.append(instance)
    
    # ── JSON-LD 조합 ──
    jsonld = {
        "@context": {
            "onto": "http://yourcompany.com/ontology#",
            "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
            "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
        },
        "@graph": graph
    }
    
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(jsonld, f, ensure_ascii=False, indent=2)
    
    print(f"변환 완료: {len(graph)}개 항목 → {output_path}")
    return jsonld

# 실행
# excel_to_jsonld("ontology_classes.xlsx", "ontology.json")

사용 방법:

1. 위의 엑셀 3시트를 채운다
2. 파일명을 "ontology_classes.xlsx"로 저장한다
3. Python 스크립트를 실행한다
4. "ontology.json" 파일이 생성된다

→ 엑셀을 수정할 때마다 스크립트를 다시 실행하면 
   JSON-LD가 자동으로 업데이트됩니다

7. 구현 시 자주 발생하는 오류 7가지

처음 구현하면 반드시 한두 가지는 겪는 오류입니다. 미리 알고 대비하세요.

오류 1: 클래스와 인스턴스 ID 중복

문제: class_id "C003"과 inst_id "C003"이 겹침
증상: 검색 시 클래스와 인스턴스가 혼동됨

해결: ID 체계를 명확히 분리
  클래스: C001, C002, C003...
  인스턴스: I001, I002, I003...
  관계: R001, R002, R003...

오류 2: 순환 참조 (Circular Reference)

문제: A caused_by B, B caused_by C, C caused_by A
증상: 추론 엔진이 무한 루프에 빠짐

해결: 인과관계는 반드시 한 방향으로만 흐르도록 설계
  ✅ 고장 → 원인 → 근본원인 (한 방향)
  ❌ 고장 ↔ 원인 (양방향 인과)

오류 3: 동의어 미등록으로 검색 실패

문제: "CNC"로 등록했는데, 사용자가 "씨엔씨"로 검색
증상: AI가 "해당 설비를 찾을 수 없습니다" 응답

해결: synonyms 필드에 가능한 모든 표현을 등록
  synonyms: ["CNC", "씨엔씨", "선반", "CNC선반", "CNC 선반"]

오류 4: 날짜 형식 불일치

문제: 어떤 셀은 "2024-03-15", 어떤 셀은 "24.3.15"
증상: Python 변환 시 에러, 날짜 비교 불가

해결: ISO 8601 형식(YYYY-MM-DD)으로 통일
  엑셀 셀 서식 → 사용자 지정 → "yyyy-mm-dd"

오류 5: 관계 방향 오류

문제: "CNC 1호기 → caused_by → 스핀들과열" (방향 반대)
정확: "스핀들과열 → caused_by → 윤활부족"

해결: 관계를 자연어 문장으로 읽어본다
  "스핀들과열은 윤활부족에 의해 발생했다" ✅
  "CNC 1호기는 스핀들과열에 의해 발생했다" ❌ (말이 안 됨)

오류 6: 빈 셀 처리 누락

문제: 인스턴스의 일부 속성이 비어있을 때 에러 발생
증상: JSON 변환 시 null 값으로 인한 오류

해결: 변환 코드에서 빈 값 체크 필수
  if value is not None and value != "":
      instance[key] = value

오류 7: 계층 구조 깨짐

문제: parent_class에 존재하지 않는 class_id를 입력
  예: C003의 parent가 "C099"인데 C099가 정의되지 않음
증상: 트리 구조가 끊어져서 상위 개념 탐색 불가

해결: 시트 1 완성 후 "참조 무결성 검사" 실행
  → 모든 parent_class 값이 실존하는 class_id인지 확인
  → Excel VLOOKUP으로 간단히 검증 가능:
  =IF(ISNA(VLOOKUP(D2,$A:$A,1,FALSE)),"❌ 오류","✅ OK")

8. 4일차 핵심 정리 & 5일차 예고

✅ 오늘 배운 것

  1. 엑셀 3시트(클래스, 관계, 인스턴스)로 온톨로지를 구현할 수 있다
  2. JSON-LD는 AI 시스템 연동에 적합한 표준 형식이다
  3. 인스턴스는 우선순위를 정해 단계적으로 입력한다 (설비 → 고장 → 부품 → 센서)
  4. Python 추론 엔진으로 교체 시기 알림, 센서 이상 감지, 고장 원인 진단이 가능하다
  5. 엑셀 → JSON-LD 자동 변환 스크립트로 유지보수를 효율화한다
  6. 구현 시 7가지 흔한 오류를 미리 알고 대비하면 시행착오를 줄일 수 있다

📌 오늘의 실습 과제

과제 1: 엑셀 파일 생성 → 시트 1(Classes)에 클래스 15개 이상 입력
과제 2: 시트 2(Relations)에 객체 관계 5개 + 데이터 속성 5개 입력
과제 3: 시트 3(Instances)에 우리 회사 설비 5대 이상 입력
과제 4: (선택) Python 추론 엔진 코드 실행해보기
과제 5: (선택) 엑셀 → JSON-LD 변환 스크립트 실행해보기

과제 1~3만 완료해도 5일차 AI 연동에 필요한 모든 준비가 끝납니다.

📅 5일차 예고: AI 연동 — 온톨로지를 AI에 먹이는 3가지 방법

다음 글에서는 오늘 만든 온톨로지를 실제 AI 시스템에 연동합니다:

  • 프롬프트 엔지니어링으로 Claude/ChatGPT에 온톨로지 적용하기
  • RAG(검색 증강 생성)과 온톨로지 결합하기
  • 실제 질문 10개로 Before/After 비교
  • 프롬프트 템플릿 (복사해서 바로 사용 가능)

💡 내일이 이 시리즈에서 가장 실감나는 날입니다.
직접 만든 온톨로지로 AI가 우리 회사 맞춤 답변을 하는 순간을 경험합니다.


자주 묻는 질문 (FAQ)

Q1. 엑셀과 JSON-LD 중 어떤 것을 써야 하나요?

둘 다 쓰세요. 엑셀은 '원본 관리용', JSON-LD는 'AI 연동용'입니다. 엑셀이 마스터 파일이고, JSON-LD는 엑셀에서 자동 변환하여 생성합니다. 현업 담당자는 엑셀로 수정하고, 개발자는 JSON-LD를 시스템에 연동합니다. 만약 팀에 개발자가 없다면 엑셀만으로도 5일차 AI 연동이 가능합니다.

Q2. 인스턴스를 수백 개 넣어야 하나요?

처음에는 아닙니다. 주요 설비 10대 + 대표 고장 이력 20건이면 PoC(Proof of Concept)에 충분합니다. AI에게 "패턴"을 보여줄 수 있는 최소한의 양이면 됩니다. 데이터가 많을수록 좋지만, 100개의 정확한 데이터가 1,000개의 부정확한 데이터보다 낫습니다.

Q3. 기존 ERP/MES 데이터를 온톨로지로 변환할 수 있나요?

가능합니다. ERP 품목 마스터 → 클래스 계층, MES 설비 마스터 → 설비 인스턴스, 불량 이력 → 고장 인스턴스로 매핑합니다. 다만 기존 시스템의 분류 체계와 온톨로지의 클래스가 정확히 일치하지 않을 수 있으므로, 매핑 테이블(ERP 코드 ↔ 온톨로지 class_id)을 만들어야 합니다. 이 작업은 한 번만 하면 이후에는 자동화 가능합니다.

반응형
반응형