Loading the catalog…
Loading the catalog…
팀 프로젝트 잇다(itda) 를 정리한다. 치매 환자 보호자가 말하듯 쓴 메모를 로컬 LLM 이 정해진 JSON으로 바꾸고, 진료일에 의사용 경과 요약지로 묶는 온디바이스 AI 서비스다. Gemma 4 E4B QLoRA 파인튜닝, GGUF 양자화, Ollama 구조화 출력, 평가 설계까지 전 과정을 따라간다. 1. 문제 정의: 왜 로컬 LLM인가 진료실에서 "요즘 어떠셨어요?"라는 질문에 보호자가 할 수 있는 답은 "좀 나빠지신 것 같아요" 정도다. 야간 각성이 몇 번이었는지, 약을 바꾼 뒤 무엇이 달라졌는지는 기억 속에 흩어져 있다. 그렇다고 체크리스트 앱은 입력 부담이 커서 오래 쓰기 어렵다. 여기서 요구사항 네 개가 나온다. ID 요구사항 기술 결정 R1 입력 부담 최소화 자유 서술 메모 → LLM 추출 R2 로컬 처리 4B급 모델 + Ollama, 외부 호출 없음 R3 의사용 경과 요약 임상 틀(NPI 영역) 기준 12개 유형 R4 신뢰할 수 있는 집계 숫자·판단은 코드, 승인된 사건만 집계 R2 가 가장 강한 제약이다. 돌봄 기록은 민감 정보이고, 기록 대상자는 외부 전송 여부를 스스로 판단하기 어렵다. 이 조건 하나가 모델 크기, 서빙 도구, 양자화 방식을 모두 정한다. 2. LLM에게는 "추출"만 맡긴다 모델이 하는 일은 메모 한 건을 사건 목록 JSON으로 바꾸는 것뿐이다. 날짜 계산, 비율, 증가 판단은 전부 코드가 맡는다. 소형 모델은 날짜와 숫자에서 실수가 잦기 때문이다. 입력 "어젯밤 두 시쯤 깨셔서 한참 거실 왔다갔다 하심. 저녁 약은 안 드신다고 버티심." 출력 {"events": [ {"type": "night_waking", "status": "present", "time_expr": "어젯밤", "count": 1, "evidence": "어젯밤 두 시쯤 깨셔서"}, {"type": "wandering_exit", "status": "present", "time_expr": "어젯밤", "count": 1, "evidence": "한참 거실 왔다갔다 하심"}, {"type": "medication_refusal", "status": "present", "time_expr": null, "count": 1, "evidence": "저녁 약은 안 드신다고 버티심"} ]} type : 12개 코드의 닫힌 목록이다. 자유 텍스트면 집계가 불가능하다. time_expr : 원문 표현 그대로. 날짜 변환은 모델 몫이 아니다. evidence : 원문을 고치지 않고 복사한다. 확인 카드 대조와 자동 검수에 쓴다. 스키마보다 어려운 것은 경계 규칙 이다. "밤에 깨서 돌아다님"은 야간 각성과 배회 둘 다로 기록하고, "저녁은 반 공기 드심"처럼 양만 적힌 문장은 추출하지 않는다. "~밖에"처럼 줄었다는 표현이 있어야 식사량 감소다. 이런 규칙을 시스템 프롬프트와 학습 데이터에 똑같이 넣는다. 3. 합성 데이터: 정답을 먼저 정한다 실제 메모는 개인정보라 모을 수 없다. 그래서 정답 JSON을 먼저 뽑고, Claude로 문장만 만든다. LLM이 라벨을 정하지 않으니 라벨 오류가 구조적으로 줄어든다. 짚고 넘어갈 설계는 두 가지다. 첫째, 메모 20%에 반복 질문·낮잠·의문문 같은 방해 요소(distractor) 를 섞어 "뽑지 말아야 할 것"을 학습시킨다. 둘째, 문체 13종을 학습·검증·평가로 완전히 분리 한다. 평가 점수가 "본 적 없는 문체"에 대한 일반화를 뜻하게 하려는 장치다. 4. QLoRA 학습과 양자화 QLoRA 는 4bit로 양자화한 베이스 모델 위에 LoRA(작은 보조 가중치)만 학습하는 방식이다. Unsloth로 RTX 4070 Ti 12GB 한 장에서 학습한다. r 16, alpha 16, 학습률 2e-4, 실효 배치 8, 2에폭(약 494스텝)이다. 핵심은 손실을 JSON 응답에만 거는 것 ( train_on_responses_only )이다. 시스템 프롬프트와 메모를 외우지 않고 추출만 배운다. W&B 기준 eval loss는 0.0163 → 0.0119로 내려가고, grad_norm 최대 0.34, NaN 0건으로 안정적이다. 학습 후 LoRA를 병합해 GGUF Q8_0을 만들고, llama.cpp로 Q4_K_M 까지 내린다. F1은 0.003만 내주고 크기와 CPU 최대 처리 시간을 크게 줄인다. 보호자 PC에는 보통 GPU가 없으니 CPU 기준이 서비스 기준이다. 5. Ollama 서빙: 직접 돌려봐야 보이는 함정 Ollama format 에 JSON 스키마를 넣으면 출력 형식이 강제된다. 아래가 실제 백엔드 호출 형태다. import json import ollama res = ollama.chat( model="itda-gemma4-e4b-q4_k_m", # 파인튜닝 후 Ollama에 등록한 이름 messages=[{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": memo_text}], format=EVENT_SCHEMA, # JSON 스키마로 출력 강제 options={"temperature": 0}, think=False, # 생각 모드를 꺼야 학습 형식대로 답한다 ) events = json.loads(res["message"]["content"])["events"] 서빙 단계에서 만나는 함정은 세 가지다. 필드 순서 : 스키마를 강제하면 Qwen·EXAONE은 필드가 알파벳순으로 바뀌어 학습 순서와 어긋나고 성능이 떨어진다. Gemma 계열은 순서를 유지한다. 필드 순서도 학습의 일부다. 채팅 템플릿 : EXAONE은 Modelfile의 TEMPLATE을 무시하고 생각 모드가 켜진 채 등록되어 F1이 0.24까지 떨어진다. 그래서 Modelfile을 손으로 쓰지 않고 GGUF 안의 jinja 템플릿을 렌더링해 만든다. think 옵션 : 베이스 모델은 생각 모드가 켜져 있으면 384토큰 안에 JSON을 끝내지 못한다(통과율 13%). 같은 데이터로 학습한 후보를 서비스 조건에서 비교하면 Gemma 4 E4B Q4_K_M이 정확도와 CPU 속도의 균형이 가장 좋다. 6. 평가: 네 가지 질문으로 나눈다 "점수가 올랐다"는 한 줄로는 부족하다. 학습이 안정적인지, 목표 능력이 생겼는지, 기존 능력이 유지되는지, 사람이 봐도 나은지를 따로 묻는다. F1은 0.857 → 0.986, 메모 완전일치는 65% → 95%로 오른다. 처리 시간은 노트북 GPU 1.6초/건, CPU만 쓰면 7.8초/건이다. 베이스가 가장 약한 과민·짜증, 사람·장소 혼동, 망상은 모두 경계 규칙이 걸린 유형이다. 과민·짜증은 초조·공격(몸으로 드러나는 행동)과, 혼동은 망상(사실이 아닌 믿음)과 헷갈리기 쉽다. 파인튜닝 후 셋 다 0.99 이상으로 오른다. 기존 능력은 일반 질문 30 + KMMLU 50문항으로 확인한다. 70.0% → 68.8%로 −1.2%p, 허용 기준 3%p 안이다. 일반 질문에 추출 JSON으로 답하는 "모드 고착"도 0%다. 채점 로직은 "같은 메모 안에서 type 과 status 가 같으면 정답"이다. 아래는 이를 단순화한 실행 가능한 버전이다. from collections import Counter def event_keys(events): # 같은 메모 안에서 (type, status)가 같으면 같은 사건 return Counter((e["type"], e["status"]) for e in events) def score(gold_memos, pred_memos): tp = fp = fn = exact = 0 for gold, pred in zip(gold_memos, pred_memos): g, p = event_keys(gold), event_keys(pred) hit = sum((g & p).values()) tp += hit fp += sum(p.values()) - hit fn += sum(g.values()) - hit exact += int(g == p) # 메모 완전일치 precision = tp / (tp + fp) if tp + fp else 0.0 recall = tp / (tp + fn) if tp + fn else 0.0 f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0 return {"precision": round(precision, 3), "recall": round(recall, 3), "f1": round(f1, 3), "exact_match": round(exact / len(gold_memos), 3)} def evidence_in_memo(memo, events): # 근거 구절이 원문에 글자 그대로 있는지 return all(e["evidence"] in memo for e in events) memo = "어젯밤 두 시쯤 깨셔서 한참 거실 왔다갔다 하심. 저녁 약은 안 드신다고 버티심." gold = [[ {"type": "night_waking", "status": "present", "evidence": "어젯밤 두 시쯤 깨셔서"}, {"type": "wandering_exit", "status": "present", "evidence": "한참 거실 왔다갔다 하심"}, {"type": "medication_refusal", "status": "present", "evidence": "저녁 약은 안 드신다고 버티심"}, ]] pred = [[ {"type": "night_waking", "status": "present", "evidence": "어젯밤 두 시쯤 깨셔서"}, {"type": "agitation", "status": "present", "evidence": "한참 거실 왔다갔다 하심"}, # 오분류 {"type": "medication_refusal", "status": "present", "evidence": "저녁 약은 안 드신다고 버티심"}, ]] print(score(gold, pred)) # {'precision': 0.667, 'recall': 0.667, 'f1': 0.667, 'exact_match': 0.0} print(evidence_in_memo(memo, pred[0])) # True 사건 하나만 틀려도 메모 완전일치는 0이다. F1과 완전일치를 함께 보는 이유다. 7. 판단은 코드가 한다: p-관리도 요약지의 "증가" 표시는 LLM이 아니라 p-관리도 (의료 품질 관리에서 비율 변동을 감시하는 통계 기법)로 계산한다. 분모는 기록한 날만 쓰고, 기록이 적을수록 문턱이 올라가 우연히 튄 값에 표시가 붙지 않는다. import math def upper_limit(p_bar, n, sigma=3): # 관리 상한 = p̄ + 3·√(p̄(1−p̄)/n) return p_bar + sigma * math.sqrt(p_bar * (1 - p_bar) / n) for n in (60, 20): ucl = upper_limit(0.10, n) need = math.floor(ucl * n) + 1 # 상한을 넘기는 최소 발생일 수 print(f"기록일 {n}일 → 상한 {ucl:.1%}, {need}일 이상이면 '증가'") # 기록일 60일 → 상한 21.6%, 13일 이상이면 '증가' # 기록일 20일 → 상한 30.1%, 7일 이상이면 '증가' 요약지 첫 장의 핵심 요약 문장은 같은 파인튜닝 모델이 쓴다. 회귀 평가에서 일반 능력 유지를 확인하므로 요약 전용 모델을 따로 두지 않고, 설치할 모델도 하나로 줄인다. 입력은 계산된 사실뿐이고, 숫자 일치·금지 표현("악화", "때문" 등)을 코드가 검사해 떨어지면 문장 틀로 대체한다. 숫자는 코드가, 문장은 모델이 맡는 분업이다. 8. 한계와 다음 과제 모든 수치는 합성 평가 세트 기준이다. 문체를 분리해도 같은 방식으로 만든 데이터라 점수가 부풀었을 수 있다. 목록 밖 관찰(반복 질문 → 식사량 감소로 오추출)도 남은 약점이다. 실제 보호자 메모로 재검증하는 것이 다음 단계다. 핵심 요약 민감 데이터 → 온디바이스 . 이 제약이 모델 크기·서빙·양자화를 결정한다. LLM은 추출만 , 날짜·숫자·판단은 코드가 맡는다. 합성 데이터는 정답 먼저, 문장 나중 . 평가 문체는 학습과 완전히 분리한다. QLoRA는 응답에만 손실을 걸고, 채팅 템플릿·필드 순서·think 옵션 을 학습과 서빙에서 일치시킨다. 평가는 안정성·목표 능력·기존 능력·사람 판단 네 갈래로 나눈다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
로컬 LLM 파인튜닝 실전 - 합성 데이터, QLoRA, Ollama 서빙까지 온디바이스 AI 프로젝트. 팀 프로젝트 잇다(itda) 를 정리한다. 치매 환자 보호자가 말하듯 쓴 메모를 로컬 LLM 이 정해진 JSON으로 바꾸고, 진료일에 의사용 경과 요약지로 묶는 온디바이스 AI 서비스다. Gemma 4 E4B QLoRA 파인튜닝, GGUF 양자화, Ollama 구조화 출력, 평가 설계까지 전 과정을 따라간다. 1. 문제 정의: 왜 로컬 LLM인가 진료실에서 "요즘 어떠셨어요?"라는 질문에 보호자가 할 수 있는 답은 "좀 나빠지신 것 같아요" 정도다. 야간 각성이 몇 번이었는지, 약을 바꾼 뒤 무엇이 달라졌는지는 기억 속에 흩어져 있다. 그렇다고 체크리스트 앱은 입력 부담이 커서 오래 쓰기 어렵다.…
Open source