병원·연구소에는 "사람이 쓴 글"로 된 의료 데이터가 산더미입니다 — 퇴원 요약지, 처방전, 검사 소견. 이걸 컴퓨터가 다루려면 먼저 "이 단어는 병명, 저 단어는 약 이름"처럼 의미를 붙여야 하고, 동시에 "이 이름·주민번호는 가려야 한다"는 개인정보 보호도 해야 합니다. 보통은 이걸 클라우드 의료 API에 보내 처리하는데, 그 순간 환자 정보가 외부 서버로 나갑니다.
OpenMed는 이 두 가지(의료 개체 추출 + 개인정보 비식별화)를 내 컴퓨터 안에서 끝냅니다. analyze_text("...") 한 줄이면 질병·약물·해부학·유전자 같은 개체를 뽑아 주고, deidentify("...") 한 줄이면 이름·날짜·주민번호를 가려 줍니다. 모델은 HuggingFace의 OpenMed 조직에 공개된 1,000개 이상에서 골라 쓰고, 네트워크 없이 로컬 폴더만 가리켜도 동작합니다.
from openmed import analyze_text
result = analyze_text(
# 만성 골수성 백혈병에 imatinib을 시작했다는 임상 문장
"Patient started on imatinib for chronic myeloid leukemia.",
model_name="disease_detection_superclinical",
)
# DISEASE chronic myeloid leukemia 0.98
# DRUG imatinib 0.95
지금까지는 환자 차트를 봉투에 넣어 외부 판독 센터(클라우드 API)로 보내고, 결과를 받아 봤습니다. 빠르지만 차트가 건물 밖으로 나가고, 보낼 때마다 비용이 듭니다.
OpenMed는 그 전문가를 병원 안에 직접 들이는 것입니다. 차트는 건물 밖으로 한 발짝도 나가지 않고, 판독은 그 자리에서 끝나며, 횟수당 요금도 없습니다. 게다가 이 전문가는 질병·약물·유전자별로 1,000명이 대기 중이고, 12개 언어를 합니다.
entity는 텍스트·라벨·신뢰도를 담는다.주목 이유는 크게 셋입니다. ① 규제·프라이버시 압박 — 환자 정보(PHI)를 외부 클라우드로 보내는 것 자체가 HIPAA·GDPR 리스크라, "데이터를 안 내보내는" 도구 수요가 큽니다. ② 모델 생태계 — 1,000개 이상의 전용 의료 모델을 하나의 API로 묶었고, 다수가 독점 스택을 능가하는 SOTA(최첨단) 성능을 냅니다. ③ 온디바이스 가속 — Apple Silicon에서 MLX로 CPU 대비 24~33배 빨라져, 아이폰에서도 실시간 비식별화가 돕니다.
| 항목 | 클라우드 의료 API | OpenMed |
|---|---|---|
| 실행 위치 | 벤더 서버 | ○ 내 기기 / 내 서버 |
| 환자 데이터 전송 | 벤더로 전송됨 | ○ 절대 나가지 않음 |
| 비용 | 호출당 과금 | ○ 무료 · 오픈소스 |
| 전용 의료 모델 | 제한적 | ○ 1,000개+ |
| 지원 언어 | 제각각 | ○ 12개 언어 |
| 오프라인 / 망분리 | ✕ | ○ 완전 오프라인 가능 |
| Apple Silicon(MLX) | 해당 없음 | ○ 24~33배 가속 |
| 네이티브 iOS/macOS 앱 | ✕ | ○ OpenMedKit |
| 벤더 종속 | 있음 | ○ 없음(Apache-2.0) |
같은 모델 이름을 적어도, Apple Silicon에서는 MLX 가속 체크포인트로, 그 외 하드웨어에서는 자동으로 똑같은 PyTorch 체크포인트로 대체되어 돕니다(한 번 경고만 띄우고). 개발자는 "어디서 돌릴지"를 신경 쓰지 않고 한 줄만 쓰면 됩니다. CoreML 폴백 경로까지 있어 iOS 앱에도 그대로 들어갑니다.
비식별화는 단순히 [NAME]으로 지우는 것 말고도, Faker 기반으로 같은 형식의 그럴듯한 가짜 값으로 바꿀 수 있습니다. 게다가 브라질 CPF/CNPJ, 네덜란드 BSN, 프랑스 NIR, 이탈리아 Codice Fiscale, 인도 Aadhaar, 독일 Steuer-ID, 미국 NPI 같은 국가별 임상 식별자 생성기를 내장해, 다운스트림 시스템이 깨지지 않는 "현실적인 가짜 데이터셋"을 만듭니다.
영어뿐 아니라 fr·de·it·es·nl·hi·te·pt·ar·ja·tr까지 12개 언어로 PII 추출·비식별화가 됩니다. extract_pii(text, lang="pt")처럼 언어만 바꾸면 포르투갈어 CPF·전화번호를 알아서 잡습니다. 언어×개체 조합으로 247개 체크포인트가 준비되어 있습니다.
OpenMed가 화제인 이유는 "성능 좋은 의료 모델"이어서가 아니라, "규제가 가장 까다로운 데이터(환자 정보)를 외부로 안 보내고도 최첨단 처리를 가능하게 했다"는 점이다. 프라이버시(온디바이스)와 성능(SOTA 모델·MLX 가속)을 동시에 잡은 것이 핵심이다.
OpenMed의 설계 철학은 "얇은 핵심 + 갈아끼우는 백엔드 + 선택적 확장"입니다. 코어는 의존성이 단 2개(pysbd, faker)뿐이고, 무거운 ML 런타임(transformers·mlx·coreml)은 전부 선택 설치(extras)로 빠져 있습니다. 필요한 백엔드만 골라 깔면 됩니다.
| 항목 | 내용 |
|---|---|
| 주 언어 | Python 3.10+ (라이브러리·REST 서비스) + Swift (네이티브 앱 OpenMedKit) |
| 빌드 백엔드 | hatchling — 버전은 openmed/__about__.py에서 동적으로 읽음 |
| 패키지 배포 | PyPI openmed + Swift Package Manager(SPM) |
| 핵심 의존성 | 단 2개 — pysbd(문장 경계 분할), faker(가짜 데이터 생성) |
| 코어 외 모든 것 | 선택 설치(extras): hf · service · gliner · mlx · coreml · docs |
| 라이선스 | Apache-2.0 (상업 이용·재배포 자유) |
| extra | 설치 명령 | 들어오는 것 |
|---|---|---|
| hf | pip install "openmed[hf]" | transformers · huggingface-hub · accelerate · tokenizers (기본 추론 런타임) |
| service | "openmed[hf,service]" | fastapi · uvicorn (REST API 서버) |
| mlx | "openmed[mlx]" | mlx · tiktoken · safetensors (Apple Silicon 가속) |
| gliner | "openmed[gliner]" | gliner · torch (제로샷 NER) |
| coreml | "openmed[coreml]" | coremltools · torch (iOS/macOS용 모델 변환) |
| docs | "openmed[docs]" | mkdocs-material (문서 사이트 빌드) |
| 백엔드 | 역할 |
|---|---|
| PyTorch (torch/) | 기본 엔진 — CPU·CUDA GPU. transformers 토큰 분류 파이프라인 |
| MLX (mlx/) | Apple Silicon 전용 가속 — CPU 대비 24~33배. PII·Privacy Filter 계열 |
| CoreML (coreml/) | iOS/macOS 네이티브 런타임 변환 — OpenMedKit의 폴백 경로 |
| GLiNER (zero_shot/) | 학습 없이 라벨만 주면 잡는 제로샷 NER (실험적) |
pip install openmed만 하면 transformers·torch 같은 수 GB짜리 의존성이 안 깔린다. 코어는 문장 분할(pysbd)과 가짜 생성(faker)만 안다. 실제 모델을 돌릴 때 비로소 [hf]나 [mlx]를 더한다. 덕분에 "비식별화 후처리 로직만 쓰고 싶은 사람"과 "GPU로 대량 추론하는 사람"이 같은 패키지를 다른 무게로 쓸 수 있다.
라이브러리를 넘어 FastAPI 기반 REST 서비스로도 돌릴 수 있습니다. Docker 이미지는 python:3.11-slim 베이스에 CPU 전용 PyTorch를 깔고, uvicorn으로 openmed.service.app을 8080 포트에 띄웁니다. HEALTHCHECK로 /health를 주기 점검하고, OPENMED_SERVICE_KEEP_ALIVE 환경변수로 유휴 모델을 자동 내려 메모리를 회수합니다.
# 서비스 실행
uvicorn openmed.service.app:app --host 0.0.0.0 --port 8080
# 또는 Docker로
docker build -t openmed:1.5.5 .
docker run --rm -p 8080:8080 -e OPENMED_PROFILE=prod openmed:1.5.5
# 엔드포인트: /health /analyze /pii/extract /pii/deidentify
# /models/loaded /models/unload (모델 생명주기 관리)
같은 모델을 아이폰·아이패드·맥 앱에 그대로 넣을 수 있는 게 큰 강점입니다. swift/ 아래에 SPM 패키지 OpenMedKit과 데모 앱 2개(OpenMedDemo, OpenMedScanDemo)가 있습니다. 앱에 다음 한 줄로 의존성을 추가하면, PII 탐지·임상 추출이 완전 오프라인으로 기기 안에서 돕니다.
// Swift Package에 OpenMedKit 추가
dependencies: [
.package(url: "https://github.com/maziyarpanahi/openmed.git", from: "1.5.5"),
]
OpenMed의 진짜 배울 점은 "임상 텍스트 → 토큰 분류 → 개체 디코딩/병합 → 출력"의 파이프라인을, 백엔드와 언어에 독립적으로 추상화한 방식입니다. 어떤 엔진(torch/mlx/coreml)을 쓰든, 어떤 언어든, 같은 analyze_text() / extract_pii() 진입점으로 흐릅니다.
core/backends.py가 "어느 엔진으로 돌릴지"를 한 곳에서 결정합니다. 사용자가 MLX 모델 이름을 적었는데 Apple Silicon이 아니면, 같은 가중치의 PyTorch 체크포인트로 자동 대체합니다. 개발자 코드는 그대로 두고 실행 환경만 바뀌어도 동작하는 "write once, run anywhere"의 ML 버전입니다.
-mlx-8bit)도 제공한다.HuggingFace의 긴 체크포인트 경로를 매번 외울 수는 없습니다. core/model_registry.py(약 22KB)는 disease_detection_superclinical 같은 친근한 별명 → 실제 HF 체크포인트 매핑을 큐레이션해 둡니다. 처음 쓸 때 모델을 내려받아 로컬 캐시에 두고, 그다음부터는 네트워크 없이 씁니다.
| 모델 별명 | 전문 분야 | 개체 타입 | 크기 |
|---|---|---|---|
| disease_detection_superclinical | 질병·상태 | DISEASE, CONDITION, DIAGNOSIS | 434M |
| pharma_detection_superclinical | 약물 | DRUG, MEDICATION, TREATMENT | 434M |
| pii_superclinical_large | 개인정보 | NAME, DATE, SSN, PHONE, EMAIL... | 434M |
| anatomy_detection_electramed | 해부학 | ANATOMY, ORGAN, BODY_PART | 109M |
| gene_detection_genecorpus | 유전자·단백질 | GENE, PROTEIN | 109M |
망 분리 환경이라면 model_id="./models/..."처럼 로컬 디렉토리를 가리켜 HF Hub에 접속하지 않고 로드할 수 있습니다.
Transformer는 단어를 서브워드 토큰으로 쪼개 처리합니다. 그래서 01/15/1970 같은 날짜가 01, /, 15... 로 흩어지기 쉽습니다. core/pii_entity_merger.py(약 28KB)는 이렇게 조각난 토큰을 다시 하나의 개체로 붙여, 날짜·주민번호가 토막 나지 않게 합니다. 비식별화 품질을 좌우하는 숨은 핵심입니다.
core/anonymizer/는 찾아낸 PII를 어떻게 처리할지 4가지 방법을 제공합니다. 상황(연구용 공유, 로그 보관, 종단 추적)에 따라 고릅니다.
| method | 동작 | 쓰임새 |
|---|---|---|
| mask | [NAME], [DATE]로 라벨 치환 | 가장 단순·안전한 가림 |
| replace | Faker로 형식 보존 가짜값 | 현실적 테스트 데이터셋 |
| hash | 암호학적 해시로 치환 | 같은 사람=같은 해시(연결 추적) |
| shift_dates | 날짜를 일정 일수만큼 이동 | 시간 간격은 보존, 실제 날짜는 숨김 |
비식별화 모델 중 일부는 OpenAI Privacy Filter 아키텍처를 공유합니다. gpt-oss 계열의 희소 MoE 트랜스포머에 로컬 어텐션·sink 토큰·RoPE+YaRN·tiktoken o200k_base를 쓰는 구조입니다. 코드는 똑같고 학습 데이터(가중치)만 다른 3가지 변형이 있으며, 모두 같은 extract_pii() API로 부릅니다 — model_name=만 바꾸면 됩니다.
| 변형 | 특징 |
|---|---|
| OpenAI Privacy Filter | 원본 베이스라인 |
| Nemotron-PII 파인튜닝 | NVIDIA Nemotron PII 데이터셋으로 미세조정 |
| OpenMed Multilingual | 12개 언어 다국어 비식별화 |
| 파일 | 역할 |
|---|---|
| service/app.py | FastAPI 앱 — 엔드포인트 정의·에러 봉투 통일 |
| service/runtime.py | 파이프라인 사전 로드·공유, 모델 적재/해제 |
| service/keep_alive.py | 유휴 시간 지나면 모델 자동 언로드(메모리 회수) |
| service/schemas.py | 요청/응답 검증 스키마(Pydantic) |
"얇은 코어 + 백엔드별 폴더 + 서비스/앱 분리" 철학이 구조에 그대로 보입니다. 두뇌는 openmed/core/에 모여 있고, 엔진은 torch/ mlx/ coreml/ zero_shot/으로 갈라지며, 배포 형태(서비스·Swift 앱)는 바깥에 있습니다.
처음 본다면 README → openmed/__init__.py(어떤 함수가 공개 API인가) → core/model_registry.py(어떤 모델이 어떤 별명인가) → core/backends.py(엔진이 어떻게 갈리나) → core/pii.py와 pii_entity_merger.py(비식별화의 심장) → service/app.py(서비스로 어떻게 노출되나) 순서가 좋다.
"문장 → 토큰별 라벨 → 개체로 합치기"는 정보 추출의 기본기입니다. OpenMed는 의료 도메인으로 이걸 실전 수준까지 끌고 갑니다. transformers의 토큰 분류 파이프라인이 어떻게 NER이 되는지, 라벨 체계(DISEASE/DRUG...)가 어떻게 정의되는지 통째로 볼 수 있습니다.
transformers의 pipeline("token-classification")으로 아무 영어 문장에서 사람·장소를 뽑아 보고, OpenMed의 analyze_text 결과와 출력 형태를 비교한다.같은 모델을 CPU·GPU·MLX·CoreML로 돌리는 추상화는, 의존성 역전과 어댑터 패턴의 ML판입니다. backends.py 한 파일이 "어느 엔진을 고를지"를 캡슐화해, 상위 코드가 하드웨어를 몰라도 되게 합니다.
"무엇이 개인정보인가", "어떻게 가려야 재식별이 안 되나"는 실무에서 가장 까다로운 주제입니다. HIPAA의 18개 Safe Harbor 식별자, 마스킹 vs 가짜치환 vs 해시의 트레이드오프, 형식 보존의 필요성을 코드로 익힐 수 있습니다.
method="mask", "replace", "hash"를 각각 적용해 출력 차이를 비교하고, "어떤 상황에 무엇을 써야 하나"를 표로 정리한다.서브워드 토큰화 때문에 날짜·번호가 조각나는 문제는 NER 품질의 숨은 적입니다. pii_entity_merger.py는 이 "조각을 다시 붙이는" 후처리가 왜 필수인지, 어떻게 구현하는지 보여 주는 좋은 교본입니다.
"010-1234-5678"을 토큰화해 몇 조각으로 쪼개지는지 보고, 인접한 같은-라벨 토큰을 다시 합치는 작은 병합 함수를 직접 짠다.모델을 "라이브러리"가 아니라 "API 서비스"로 만들 때 생기는 문제 — 사전 로드, 메모리 회수, 헬스체크, 컨테이너화 —를 한 레포에서 다룹니다. runtime.py의 파이프라인 공유와 keep_alive.py의 유휴 언로드가 특히 실전적입니다.
curl로 /pii/extract를 호출한 뒤, /models/loaded·/models/unload로 메모리에 올라온 모델을 직접 내려 본다.| 항목 | 요구사항 |
|---|---|
| 파이썬 | Python 3.10+. 코어만이면 의존성 2개로 가벼움 |
| 추론(기본) | openmed[hf] — transformers·torch. 434M 모델은 메모리 ~2GB 안팎 권장 |
| Apple 가속 | openmed[mlx] + Apple Silicon(M1 이상). CPU 대비 24~33배 |
| REST 서비스 | openmed[hf,service] 또는 Docker(python:3.11-slim, 8080 포트) |
| iOS / macOS 앱 | OpenMedKit(SPM), CoreML 변환 모델. iOS 17+ / macOS 14+ / 최신 Xcode |
| 모델 다운로드 | 최초 1회 HF Hub 접속(이후 로컬 캐시). 망분리 시 로컬 디렉토리 지정 |
| 네트워크 | 모델만 받으면 추론은 완전 오프라인. 데이터는 절대 외부 전송 안 함 |
OpenMed는 텍스트 처리 도구이지 의료기기·법적 컴플라이언스 인증이 아니다. ① 자동 비식별화 출력은 모델의 예측이며 100% 재식별 방지를 보장하지 않는다 — 실제 환자 데이터에 쓰기 전 반드시 사람 검수와 기관 정책(IRB·DPO) 검토를 거쳐야 한다. ② NER 결과(질병·약물)는 진단·치료 판단의 근거가 아니라 추출 보조일 뿐이다. ③ HIPAA "Safe Harbor 18종 지원"은 기능 범위를 뜻하지, 특정 데이터셋이 법적으로 비식별되었음을 보증하지 않는다. 규제 데이터에 적용할 때는 법무·보안 검토를 함께 진행하라.
pip install "openmed[hf]" 후, 합성(가짜) 임상 문장 하나를 disease_detection_superclinical로 분석해 DISEASE·DRUG 개체와 신뢰도를 출력한다. 실제 환자 데이터가 아니라 합성 문장으로 시작하는 습관을 들인다.
"Patient: John Doe, DOB: 01/15/1970" 같은 합성 문장에 mask·replace·hash·shift_dates를 각각 적용해 출력을 나란히 본다. 어떤 게 "연구 공유용", 어떤 게 "로그 보관용"에 맞는지 한 줄씩 메모한다.
uvicorn openmed.service.app:app(또는 Docker)로 서비스를 띄우고 POST /pii/extract에 JSON을 보내 응답을 확인한다. 이어 /models/loaded로 적재된 모델을 보고 /models/unload로 내려 메모리 변화를 관찰한다.
extract_pii(text, lang="pt")로 포르투갈어 CPF·전화를, lang="ja"로 일본어 マイナンバー를 잡아 본다. 언어만 바꿨을 뿐인데 국가별 식별자를 인식하는 과정을 따라가며, pii_i18n.py가 어떻게 라우팅하는지 코드를 본다.
모델을 로컬 디렉토리로 내려받은 뒤, 네트워크를 끊고 model_id="./models/..." + OpenMedConfig(device="cpu")로 HF Hub 접속 없이 추론이 되는지 검증한다. "데이터도 모델도 안 나가는" 진짜 온디바이스 환경을 직접 만들어 본다.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주차 | 트랜스포머 NER 기초 | 토큰 분류·BIO 태깅 개념, HF pipeline으로 NER 실습, 라벨 체계 이해 |
| 2주차 | PII / 비식별화 / HIPAA | Safe Harbor 18종, mask/replace/hash 트레이드오프, Faker 형식 보존, 재식별 위험 |
| 3주차 | 온디바이스 추론 · 이식성 | MLX·CoreML 변환, 8비트 양자화, 백엔드 추상화·자동 폴백 설계 |
| 4주차 | ML 서비스화 + Swift | FastAPI 서빙·모델 생명주기·Docker, OpenMedKit로 iOS 앱에 모델 임베드 |
이 레포는 "의료 NLP 모델을 어떻게 실전 제품으로 만드나"의 완결판이다. 모델 추론이라는 한 점이 아니라, 전처리→백엔드 선택→추론→개체 병합→비식별화→서빙→앱 임베드까지 한 줄로 꿰여 있다. 각 단계의 "입력과 출력"을 적어 가며 따라가면, 4주 뒤엔 다른 도메인(법률·금융 문서의 PII)으로도 같은 골격을 옮길 수 있다.
analyze_text가 돌려주는 entities는 각각 텍스트·라벨·신뢰도를 가진다.zero_shot/에 실험적으로 둔다.Dr., mg.)가 많아 마침표만으로 자르면 틀린다. OpenMed의 코어 의존성 pysbd가 규칙 기반으로 이 경계를 잡는다.