티스토리 뷰
📌 시리즈 안내 — 7일 완성 온톨로지 마스터 과정
| 일차 | 주제 | 핵심 내용 |
| 1일차 | 온톨로지 기초 | 온톨로지란? 왜 필요한가? 3대 구성요소 |
| 2일차 | 구축 프레임워크 | 5단계 프로세스, 범위 정의, 용어 수집 방법론 |
| 3일차 | 실전 설계 | 설비관리·불량분석 온톨로지 전체 설계 실습 |
| 4일차 (오늘) | 구현 실습 | 엑셀 온톨로지 완성, JSON-LD 변환, Python 코드 |
| 5일차 | AI 연동 | 프롬프트 엔지니어링 × 온톨로지, RAG 연동 |
| 6일차 | 실전 시나리오 | 불량 자동분석, 사내 AI 챗봇 구축 사례 |
| 7일차 | 30일 로드맵 | 추천 도구, 기술 스택, 주차별 실행 계획 |
💡 3일차 복습: 설비 관리·불량 분석 온톨로지를 "목표 질문 → 클래스 → 관계 → 규칙 → 추론 경로 검증" 순서로
설계했습니다. 오늘은 그 설계를 실제 파일로 만듭니다. → 3일차 다시 보기
목차
- 오늘의 목표: 동작하는 온톨로지 만들기
- 방법 ①: 엑셀 3시트 온톨로지 (비개발자용)
- 방법 ②: JSON-LD로 변환하기 (개발자용)
- 인스턴스 입력 실습 — 실제 데이터 채워넣기
- Python으로 온톨로지 자동 생성하기
- 엑셀 → JSON-LD 자동 변환 코드
- 구현 시 자주 발생하는 오류 7가지
- 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일차 예고
✅ 오늘 배운 것
- 엑셀 3시트(클래스, 관계, 인스턴스)로 온톨로지를 구현할 수 있다
- JSON-LD는 AI 시스템 연동에 적합한 표준 형식이다
- 인스턴스는 우선순위를 정해 단계적으로 입력한다 (설비 → 고장 → 부품 → 센서)
- Python 추론 엔진으로 교체 시기 알림, 센서 이상 감지, 고장 원인 진단이 가능하다
- 엑셀 → JSON-LD 자동 변환 스크립트로 유지보수를 효율화한다
- 구현 시 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)을 만들어야 합니다. 이 작업은 한 번만 하면 이후에는 자동화 가능합니다.
