Named Entity Recognition
SNOMED의 첫 단계 범주를 허용 라벨로 사용해 문장 속 생의학 언급을 찾고 유형을 부여한다.
한 단어는 짧지만 그 뒤의 세계는 깊다. “Zika”라는 네 글자도 바이러스일 수 있고, 질병일 수 있으며, 선천성 감염일 수 있다. 제8장은 이 모호함을 주변 단어만으로 풀지 않는다. 오픈 LLM의 언어 능력과 SNOMED 온톨로지의 관계 구조를 겹쳐, 이름이 놓일 정확한 자리를 찾는다.
scispaCy는 생의학 문서에 강하고 UMLS 후보를 연결할 수 있지만, 제8장은 그 능력이 특정 도메인과 주변 표현에 지나치게 기대는 지점을 짚는다.
생의학 분야에 맞춰 설계된 도구이므로 풍부한 온톨로지를 가진 다른 분야로 곧바로 옮기기 어렵다.
새 개체와 용어를 지식베이스에 확장·갱신하는 과정이 유연하지 않다.
참조 지식베이스에 들어 있는 광범위한 정보를 중의성 해소에 충분히 사용하지 않는다.
개체 사이 관계와 경로를 후보 판별의 근거로 직접 활용하지 않는다.
“Zika virus”, “congenital Zika syndrome”, “Zika virus infection”처럼 바이러스·선천성 증후군·감염이라는 단서가 표면에 드러난다. scispaCy는 주변 단어를 이용해 각각의 UMLS 후보를 올바르게 상위에 둔다.
| 언급 | 상위 후보 | UMLS CUI |
|---|---|---|
| Zika virus | Zika Virus | C0318793 |
| congenital Zika syndrome | Congenital Zika Syndrome | C4546023 |
| Zika virus infection | Zika Virus Infection | C0276289 |
새 예문은 Flaviviridae, Aedes mosquito, chikungunya fever, microcephaly, congenital malformation을 함께 언급하지만 “congenital Zika syndrome” 같은 직접 표현은 쓰지 않는다. 모델은 첫째·셋째 “Zika”를 Zika Virus Infection으로 해석하고, 둘째 문장의 “Zika disease”에는 후보를 제시하지 못한다.
주변 단어만 더 보는 것이 아니라, 문장 속 다른 의료 개체 후보들과 온톨로지에서 어떻게 연결되는지를 본다.
Recognized entity: Zika 1. C0276289 · Zika Virus Infection 2. C0318793 · Zika Virus 3. C4687930 · Zika Virus Antibody Measurement Recognized entity: Zika disease → ranked target candidate 없음 Recognized entity: Zika 1. C0276289 · Zika Virus Infection 2. C0318793 · Zika Virus 3. C4687930 · Zika Virus Antibody Measurement
제8장의 참조 지식은 SNOMED다. 이름과 별칭만 적재하는 데 그치지 않고, 루트에서 첫 단계 원형 범주를 찾고 SNOMED_IS_A 계층을 따라 그 의미 타입을 깊은 노드까지 전파한다.
sct2_Description_Full-en_US1000124_20220901.txt는 명칭·별칭 정보를, sct2_Relationship_Full_US1000124_20220901.txt는 수치 코드 기반 관계를 제공하는 것으로 구현 흐름이 설명된다.
관계 노드·엣지 생성 → 개체와 관계에 이름·별칭 부여 → SNOMED_IS_A를 이용한 첫 단계 범주 전파의 순서로 진행한다.
SnomedEntity.id의 유일성 제약과 이름·관계 ID·관계 타입·UMLS 매핑 인덱스를 만든다. 관계 파일의 각 행은 SNOMED_RELATION으로 적재되며, 타입 코드가 116680003이면 별도의 SNOMED_IS_A도 생성한다.
MERGE (source:SnomedEntity {id: row.sourceId})
MERGE (target:SnomedEntity {id: row.destinationId})
MERGE (source)-[:SNOMED_RELATION {id: row.typeId}]->(target)
// IS_A code = 116680003
FOREACH (_ IN CASE WHEN row.typeId = '116680003' THEN [1] ELSE [] END |
MERGE (source)-[:SNOMED_IS_A]->(target)
)설명 파일의 개체명과 관계명은 각각 name과 aliases로 적재된다. 같은 이름을 중복 추가하지 않고 기존 배열을 보존한다.
후보 선택은 정확히 같은 문자열만 찾는 일이 아니다. 한 개념에 붙은 다양한 표현을 검색 공간으로 열어야 한다.
루트 138875005 아래의 첫 단계 노드를 원형 범주로 잡고, APOC 경로 확장으로 모든 하위 노드를 순회한다. 각 하위 노드의 type 배열에 첫 단계 노드 이름을 넣어 NER 범주로 재사용한다.
온톨로지 적재는 검색용 사전 준비가 아니다. 이때 전파된 타입이 이후 LLM NER 프롬프트의 허용 범주 목록이 된다.
제8장은 앞 장의 OpenAI API 실험에서 한 걸음 옮겨, Ollama와 Llama 3.1 8B를 로컬에 배치한다. 외부 제공자 의존을 줄이고 데이터 통제권을 확보한다는 것이 설계의 출발점이다.
문서와 프롬프트가 외부 모델 서비스로 나가지 않도록 실행 위치를 통제한다.
모델 호출이 로컬 환경에서 이루어져 외부 API 지연과 가용성 의존을 줄인다.
Ollama의 OpenAI Chat Completions 호환 API를 사용해 기존 Python 호출 구조를 재사용한다.
ollama serve ollama pull llama3.1:latest
client = OpenAI( base_url="http://localhost:11434/v1", api_key="default" ) response = client.chat.completions.create( model="llama3.1:latest", messages=messages, temperature=0, max_tokens=4000, top_p=1 )
도서는 Llama 3.1 8B를 80억 매개변수, 최대 128,000 토큰 문맥, 다국어 정보 처리와 소비자급 하드웨어 배치를 고려한 모델로 설명한다. 예제 결과는 2024년 10월 당시 최신 Llama 3.1 모델로 얻었다고 명시한다. 이 페이지는 해당 장의 스냅샷을 그대로 해설하며 최신 모델 상태를 외부 정보로 갱신하지 않는다.
입력 문서는 곧바로 SNOMED ID가 되지 않는다. 먼저 언급을 찾고, 온톨로지에서 가능한 주소를 모으며, 후보들 사이의 관계를 읽어 한 주소를 선택한다.
SNOMED의 첫 단계 범주를 허용 라벨로 사용해 문장 속 생의학 언급을 찾고 유형을 부여한다.
Neo4j 전문 검색으로 언급과 가까운 SNOMED 개체를 찾는다. 이 단계에는 LLM을 쓰지 않는다.
후보 사이 최단경로를 탐지하고, 경로를 문장으로 바꾸고, 요약한 뒤 LLM이 최종 개체를 고른다.
NER 프롬프트는 모델이 임의의 범주를 만들어내지 못하게 한다. SNOMED에서 수집한 범주만 허용하고, 문장별 JSON으로 모든 단일 언급을 반환하게 한다.
SnomedEntity.type 배열을 펼쳐 서로 다른 범주와 개체 수를 집계한 뒤, 그 범주 목록을 NER 시스템 프롬프트에 삽입한다.
Risk factors → Events, rhinocerebral mucormycosis → Disease, poorly controlled diabetes mellitus → Disease, severe immunosuppression → Qualifier value로 구조화한다.
System 의료 도메인의 명명 개체를 추출한다. 허용 범주는 그래프에서 얻은 {named_entities}뿐이다. 모든 단일 언급을 문장별 유효 JSON으로 반환한다. Output schema { "sentence": "...", "entities": [ {"id": 0, "mention": "...", "label": "Disease"} ] }
| 언급 | 제8장 Llama 출력 라벨 | 문자 위치 |
|---|---|---|
| Zika | Organism | 19–22 |
| microcephaly | Clinical finding (finding) | 105–116 |
| congenital malformations | Clinical finding (finding) | 122–145 |
제8장은 모델이 시작·끝 문자 위치를 정확히 검출하는 데 어려움이 있다고 보고, 문자열 검색 기반 후처리 함수로 위치를 다시 계산한다. 언어 해석은 LLM에 맡기되, 결정론적으로 계산 가능한 위치는 코드가 맡는 분업이다.
시작–끝 인덱스(끝 포함): -
제8장은 후보 검색을 언어모델의 기억에 맡기지 않는다. 전체 온톨로지는 프롬프트에 넣기 너무 크고, 참조 지식에서 직접 후보를 뽑아야 검증 가능한 ID를 얻을 수 있기 때문이다.
후보는 모델 내부 지식이 아니라 SNOMED 노드에서 직접 검색한다.
45만 개가 넘는 개념 전체를 프롬프트에 넣지 않고 Neo4j 전문 인덱스를 사용한다.
NER가 부여한 라벨을 $labels로 사용해 검색 공간을 해당 타입으로 제한한다. 여러 단어의 언급은 각 단어에 약 0.80의 fuzzy 조건을 적용하고 AND로 묶는다. 도서는 전문 검색에 벡터 검색을 더하면 문자열 유사성으로 놓치는 후보를 보완할 수 있다고 제안한다.
CALL db.index.fulltext.queryNodes( "names", $fulltextQuery, {limit: $limit} ) YIELD node WHERE node:SnomedEntity AND ANY(t IN node.type WHERE t IN $labels) RETURN DISTINCT node.name, node.id // multi-word example word1~0.80 AND word2~0.80
제8장의 CS 결과를 대화형 카드로 재현한다. 이 단계는 순위를 확정하지 않고 가능한 해석을 모은다.
문맥 칩은 다음 CD 단계의 근거를 보여 주기 위한 장치다.
후보 C의 선택은 CS가 아니라 다음 단계의 온톨로지 경로와 문맥 요약을 거친 최종 결과다.
후보 해소는 세 단계로 나뉜다. 그래프 알고리즘이 길을 찾고, LLM이 그 길을 문장으로 읽으며, 또 다른 LLM 단계가 여러 문장을 한 문맥으로 압축한다.
문장 속 서로 다른 언급의 후보 쌍마다 1–2홉 최단경로를 찾고 고차수 허브를 제외한다.
노드와 방향성 관계로 된 경로를 정확한 개체명을 유지한 자연어 문장으로 바꾼다.
여러 경로 문장을 핵심 개체와 관계를 보존한 짧은 문맥으로 압축한다.
GDS degree stream으로 차수가 높은 노드 350개를 허브 목록으로 만든다. 후보 쌍 사이의 모든 최단경로를 1–2개 관계로 제한하고, 경로에 허브 이름이 하나라도 포함되면 제외한다.
(Congenital Zika virus infection)-[:OCCURRENCE]→(Congenital)←[:OCCURRENCE]-(Micrencephaly)
프롬프트는 경로의 정확한 개체명을 유지하고, 불필요한 문자를 넣지 않은 JSON을 생성하도록 지시한다. 방향과 관계 타입을 사람이 읽을 수 있는 문장으로 풀어낸다.
“선천성 지카 바이러스 감염의 occurrence가 Congenital occurrence와 연관되며, 그 Congenital occurrence는 Micrencephaly와 연관된다”는 식으로 번역된다.
경로마다 생성된 문장이 많아지면 모델의 인지 부담과 토큰 수가 증가한다. 요약 단계는 모든 중요 개체를 유지하면서 관련 문장을 하나의 context 문자열로 압축한다.
Congenital Zika virus infection이 Micrencephaly, Acrocephaly, Multiple congenital malformations, Other congenital malformations와 연관되고 이들이 Congenital 개체를 공유한다는 문맥으로 정리한다.
최종 프롬프트는 원문 문장, 언급별 SNOMED 후보, 온톨로지 경로에서 만든 문맥을 입력으로 받는다. 모델은 문맥과 가장 잘 맞는 snomed_id와 이름을 JSON으로 반환한다.
| 언급 | 최종 SNOMED 개체 | ID |
|---|---|---|
| Zika | Congenital Zika virus infection | 762725007 |
| microcephaly | Micrencephaly | 204030002 |
| congenital malformations | Multiple congenital malformations | 116022009 |
표시되는 번역과 요약은 장의 예시를 짧게 재구성한 학습용 표현이다.
최종 프롬프트 구조를 설명하기 위해 Asthma와 Allergic rhinitis가 공통 호흡기 질환 맥락을 갖는 예를 사용한다. 후보 중 일반 Asthma(195967001)와 Allergic rhinitis(61582004)를 선택한다.
microcephaly와 congenital malformations가 제공하는 관계 문맥 때문에 일반 바이러스나 일반 질병보다 Congenital Zika virus infection이 우선된다.
이 페이지는 제8장의 표현을 임의로 교정하지 않았다. 대신 독자가 구현과 결과를 혼동하지 않도록 원문 안의 명시적 불일치와 주의점을 별도로 표시한다.
도식 설명은 “Zika”를 SNOMED의 Disease 개념으로 인식한다고 서술하지만, 실제 Llama 출력 예시는 “Zika”를 Organism으로 라벨링한다. 이 페이지는 출력 표에는 Listing 8.11을 따르고 도식 설명의 차이를 별도 기록한다.
표시된 경로는 Congenital Zika virus infection과 Micrencephaly를 연결하지만, 도식 주석은 이를 “Zika virus disease”와 “Chikungunya fever” 사이 경로라고 설명한다. 경로 문자열과 설명 대상이 맞지 않는 원문상 불일치다.
입력 언급은 “microcephaly”이지만 최종 SNOMED 이름은 “Micrencephaly”로 제시된다. 오탈자 여부를 외부 자료로 교정하지 않고 도서 결과를 그대로 유지한다.
OCCURRENCE를 공유하는 2홉 경로는 관련 문맥을 제공하지만 그 자체가 인과를 보장하지 않는다. 제8장도 경로를 후보 해소의 근거로 사용하며, 관계를 곧바로 임상 인과로 확정하지 않는다.
온톨로지 릴리스, 파일 역할, 인덱스 명칭, 허브 개수 350, 경로 길이 1–2홉, fuzzy 임계 0.80, 모델·프롬프트 버전, JSON 파싱 실패 처리, 언급 offset 후처리를 실행 메타데이터로 남겨야 재현성이 생긴다.
코드 전체를 복제하지 않고 각 Listing이 파이프라인에서 맡는 역할을 정리했다. 번호를 펼치면 입력·처리·산출물의 위치를 빠르게 찾을 수 있다.
ollama serve, ollama pull llama3.1:latest로 모델을 준비하고 OpenAI 호환 API를 호출하는 LLM_Model 클래스를 구성한다.제8장의 기여는 LLM을 NED에 추가한 데 있지 않다. 결정론적 검색, 그래프 알고리즘, 자연어 변환을 역할별로 나눠 한 모델의 기억이나 한 종류의 문맥에 과도하게 의존하지 않는 구조를 만들었다는 데 있다.
주변 단어가 희박하면 동일 문자열의 바이러스·질병·선천성 감염을 구분하기 어렵다.
범주를 NER에, 명칭·별칭을 후보 검색에, 관계 경로를 최종 중의성 해소에 공급한다.
검증 가능한 ID를 얻고 거대한 온톨로지를 프롬프트에 넣지 않기 위해 Neo4j 전문 검색을 사용한다.
그래프 경로를 문장으로 옮기고 여러 문장을 짧은 문맥으로 요약해 최종 선택의 근거를 만든다.
풍부한 개체·관계 온톨로지가 존재하면 생의학 밖의 NED에도 같은 프레임을 적용할 수 있다.