AX4U 바이브코딩 마스터 과정의 비전 학습 원고. 가상 자전거 보관대 안내를 중심으로 픽셀→평가→CNN→전이학습→YOLO 라벨·학습·평가·영상→마스크→이상탐지→VLM→실패를 표시하는 로컬 프로젝트까지 연결한다. 기본 계산은 표준라이브러리로 실제 검산했고, 실제품 선택 경로는 전체 코드·합성입력·설치·검증·복구를 제공하되 라이브러리 설치/모델학습/다운로드/카메라는 제작 시 미실행이다. 합성 색상 자료의 점수를 실제 자전거 성능으로 주장하지 않는다. 원강의 전체·비공개교안 확인이나 원기관 제휴를 뜻하지 않으며 객관식만으로 숙련을 인증하지 않는다.
읽음과 숙련은 별개입니다. 자동채점은 선택형에만 적용합니다.
교육자의 독립: 개념을 이해하고 실전 사례를 직접 검증해 현실 문제를 해결하는 시스템을 스스로 개발합니다.
공동체 기여: 결과와 실패·복구 과정을 다른 사람에게 설명하고 함께 개선합니다. 이 과정은 숙련을 보장하지 않습니다.
이 기기에만 저장됩니다. 자동 동기화되지 않으며 개인정보·비밀키를 적지 마세요. 읽음·답변·증거는 숙련 인증이 아닙니다.
학습 단원
사진에게 어떤 모양의 답을 받을까
해결할 문제와 목표
가상 자전거 보관대 안내를 만들려 한다. 사진 전체에 “비었음” 하나만 붙이면 어느 자리로 가야 하는지 알 수 없고, 가린 자리를 비었다고 표시하면 헛걸음을 시킨다.
같은 장면에 분류·탐지·의미분할·인스턴스분할·VLM을 구분하고, 관측 불가를 답의 일부로 설계한다.
먼저 알아둘 것
필수 선수지식 없음
핵심 한 문장: 모델 이름보다 먼저 답의 단위와 모르는 상태를 정한다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
한 장의 지도에 도시 이름만 적을지, 건물마다 핀을 꽂을지, 땅의 경계를 칠할지를 고르는 일이다.
비유가 닿지 않는 곳: 지도는 이미 측량되었다고 생각하기 쉽지만 사진은 한 시점의 가려진 투영이며, 핀 개수가 현실 물체 총수임을 보장하지 않는다.
실제로 작동하는 원리
분류는 한 이미지 또는 사전에 잘라 놓은 자리 ROI에 라벨 하나를 낸다. 예컨대 자리별 crop을 넣으면 empty/occupied/unknown 정책을 적용할 수 있지만 사진 전체의 라벨만으로 A와 C의 위치를 얻을 수 없다. 탐지는 상자·종류·점수 목록을 낸다. 의미분할은 픽셀마다 클래스, 인스턴스분할은 같은 클래스 안에서도 개체별 ID를 낸다. VLM은 이미지와 질문으로 문장을 생성한다. 문장이 자연스럽다고 위치·개수·문자를 정확히 센 것은 아니다.
이 과정의 운영 사례는 무인 합성 보관대다. A empty, B occupied, C empty, D occluded라는 관측 메모만 주어지면 확실한 빈자리는 A/C 두 곳이고 전체 빈자리는 2~3 범위다. D가 비었는지는 새 관측 없이는 모른다. unknown은 모델이 학습한 제3클래스일 수도 있지만 여기서는 가림·오류 검사에서 강제하는 정책 상태다. 손쉽게 추가한 라벨 하나가 자동으로 불확실성을 알아내지는 않는다.
기본 경로는 모델 없이 개념과 검증을 실행한다. 선택 경로의 색깔 표식 합성 데이터는 라이브러리 흐름을 배우는 장치이며 실제 자전거 사진 데이터가 아니다. 색깔 분류기 성능을 자전거 점유 성능으로 바꾸어 말하지 않는다.
따라 하기
먼저 “자리별 현재 상태와 미확인 이유를 안내한다. 재고/장부는 변경하지 않는다”라는 요구를 적는다.
자료의 vision-task.py를 저장하고 python3 vision-task.py를 실행한다. 출력에는 관측 메모에서 계산한 범위만 있다.
A/C의 위치는 slot ID가 이미 알려졌기 때문에 가능한 출력임을 표시한다. 사진에서 슬롯 위치를 찾는 문제는 아직 풀지 않았다.
친구에게 “D 뒤에 실제 자전거가 있다면 출력 중 어떤 부분이 바뀌어야 하는가?”를 물어본다. 새 관측을 넣기 전 코드가 단정하면 실패다.
입력 예제
slots={'A':'empty','B':'occupied','C':'empty','D':'occluded'}; 각 자리에는 최대 자전거 한 대라는 교육 가정.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
visible_empty ['A', 'C'] count 2
unknown ['D'] total_empty_range (2, 3)
검증 방법
stdout에서 count=2, unknown=[D], 범위=(2,3)를 함께 확인한다. count=2만 적으면 부분 답이다.
D를 occupied로 바꿔 재실행하면 unknown은 빈 목록, 빈자리 범위는 (2,2)여야 한다. D가 empty이면 (3,3)이어야 한다.
흔한 오해와 실패 복구
탐지가 안 된 자리를 자동으로 empty로 채우지 않는다. 가림·프레임 밖·모델 실패를 별도 상태로 유지한다.
사람 없는 스케치라도 관측 시각이 오래되면 현재 상태가 아니다. 이번 고정 입력의 출력에는 실시간이라는 이름을 붙이지 않는다.
확인 문제
물체의 정확한 픽셀 경계가 필요한 요구에 가장 직접 대응하는 출력은?
자동채점 · 선택형
해설과 정답 보기
픽셀별 소속이 필요한 요구이므로 마스크다. 상자는 경계 바깥 배경도 포함한다.
정답: 픽셀 마스크
A/C가 비고 D가 가려졌을 때 왜 “빈자리 3개”라고 안내하면 안 되는가?
자기평가 · 자동채점 아님
평가 기준: 관측 2개, D 미확인, 확정 수 대신 범위 또는 미확인 안내의 세 요소를 모두 설명하면 통과. 탐지 모델 이름은 채점하지 않는다.
해설 보기
관측된 빈자리는 두 곳이다. D는 미관측이므로 2~3 범위만 가능하며 확정 3은 근거를 넘는다.
독립 실습
해설을 가리고 A=empty, B=occluded, C=occupied, D=occluded로 입력을 바꾸어 확정 빈자리 ID·전체 빈자리 범위·추가 관측 요청을 작성하라.
남길 증거: 수정한 입력 딕셔너리, 실제 stdout, B/D를 미확인으로 남긴 이유 한 문단.
기본 worked example — 표준라이브러리 실제 검산
slots = {'A': 'empty', 'B': 'occupied', 'C': 'empty', 'D': 'occluded'}
visible_empty = [k for k, v in slots.items() if v == 'empty']
unknown = [k for k, v in slots.items() if v == 'occluded']
assert visible_empty == ['A', 'C'] and unknown == ['D']
print('visible_empty', visible_empty, 'count', len(visible_empty))
print('unknown', unknown, 'total_empty_range', (len(visible_empty), len(visible_empty) + len(unknown)))
과정 사용법과 실행 경계
AX4U 바이브코딩 마스터 과정 / seeing-ai
권장 순서: 문제→픽셀→평가→CNN→전이학습→탐지→영상→마스크→이상→VLM→프로젝트. 단원별 읽기 20~40분, 독립실습 30~90분의 미실측 예상치이며 환경 준비/학습/프로젝트는 별도다. 각 단원 questions의 이유와 practice의 새 입력을 도움 없이 재현한 단원만 건너뛴다. 객관식 정답이나 체크박스만으로 숙련을 인증하지 않는다.
표준라이브러리 계산 실행, 라이브러리 코드 작성, 실제 모델 실행, 현장 검증을 각각 별도 상태로 기록한다. 기본 예제의 assert는 고정 입력용이다. 독립 실습에서 입력을 바꾸면 예상값을 먼저 새로 계산하여 assert도 그 값으로 작성하고, 원본 기본 시험은 별도 보존한다. assert 삭제만으로 통과시키지 않는다. 원강의 전체 수강·기관 제휴·공인 과정·학습자 성취를 주장하지 않는다. 원자료의 고유 사진/예제/표현을 복제하지 않았다.
실습 해설 보기
A만 확정 빈자리다. B/D가 모두 점유면 1, 모두 비었으면 3이므로 범위 1~3. B/D를 다시 촬영하거나 현장 확인을 요청한다. 분류기를 바꾸기 전에 관측 불가 정책부터 유지한다.
평가 기준: 자동 재실행: ID=[A], 범위=(1,3) 확인. 자기평가: 가림을 빈자리로 단정하지 않는 이유와 재관측 계획을 설명. 두 기준 모두 만족해야 통과.
새 맥락에 적용하기
사람 얼굴 없는 실내 화분 선반으로 바꾸어 “화분이 있나/어디 있나/흙 면적이 얼마인가/관찰문은 무엇인가”에 맞는 출력 네 가지를 적고, 가려진 화분 총수는 보류하라.
내 말로 설명하고 가르치기
학생은 지도 비유를 1분 설명한 뒤 한 이미지로 모든 답을 얻을 수 없다는 반례를 든다. 강사는 전체 분류와 자리별 crop 분류의 차이를 다시 묻는다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
읽음과 숙련은 별개입니다.
학습 단원
사진 숫자를 바꿀 때 잃는 것을 보기
해결할 문제와 목표
어두운 보관대 스케치를 밝히자 밝은 표식 두 칸이 구별되지 않는다. 색 순서를 거꾸로 읽으면 초록/빨강 상태 표식을 다른 색으로 해석할 수도 있다.
픽셀 배열·해상도·채널 순서·ROI·크기변경·클리핑을 추적하고 전처리와 학습을 구분한다.
먼저 알아둘 것
vision-task: 출력 단위와 unknown 정책
핵심 한 문장: 전처리는 숫자를 바꾸는 규칙이고, 잘린 정보는 다시 어둡게 해도 돌아오지 않는다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
각 칸에 0~255만 적을 수 있는 작은 숫자 색칠판이다. 넘치는 잉크는 컵 밖으로 버려진다.
비유가 닿지 않는 곳: 실제 색은 채널·색공간·노출·센서 응답에 영향을 받는다. 한 채널에 일정 수를 더하는 것은 조명을 물리적으로 되돌리는 복원이 아니다.
실제로 작동하는 원리
회색 영상은 높이×너비 배열, 컬러는 보통 높이×너비×채널이다. 배열[y][x]와 상자(x,y)를 뒤집지 않는다. OpenCV의 일반 컬러 읽기는 BGR, 많은 표시/학습 도구는 RGB를 쓰므로 명시적으로 변환한다. BGR (5,40,220)을 RGB로 바꾸면 (220,40,5)다. RGB→회색은 단순 채널 순서 변경이 아니라 색 정보를 버리는 변환이다.
기본 입력 [[20,230],[100,255]]에 40을 더하되 255에서 멈추면 [[60,255],[140,255]]다. 230과 255가 같은 값이 되었으므로 결과에서 40을 빼도 두 원값을 복원할 수 없다. uint8 NumPy 덧셈은 범위를 넘으면 modulo로 돌아갈 수 있다. 안전한 명시적 경로는 더 큰 정수형으로 변환→더하기→clip→uint8이다.
ROI는 원본의 일부 좌표만 남긴다. 오른쪽 열을 crop하면 [[230],[255]]이며 크기는 높이2·너비1이다. resize는 보간 규칙으로 새 숫자를 만들지 새 사실을 관측하지 않는다. 마스크 라벨을 resize할 때 bilinear는 존재하지 않는 중간 클래스 값을 만들 수 있으므로 nearest를 쓴다. 학습과 추론에서 채널 순서·정규화·crop 규칙을 같게 기록해야 한다.
따라 하기
원본 네 숫자와 +40의 잘리기 전 값을 손으로 적고 255 초과 칸에 표시한다.
python3 vision-pixels.py를 실행해 손계산과 대조한다. crop은 밝게 만들기 전 원본에서 수행하므로 두 값을 섞지 않는다.
선택: 내장 setup 안내의 OpenCV 패키지만 설치한 뒤 python optional_opencv.py. 외부 사진 없이 직접 생성한 컬러 배열을 PNG로 읽고 변환·저장한다. 실제 라이브러리 경로는 제작 시 미실행이다.
선택 경로에서는 shape/dtype, 첫 RGB 픽셀, 8×8 출력과 원본을 비교한다. 확대 그림이 매끈한지보다 원값 대응이 맞는지 확인한다.
입력 예제
기본: 회색 image=[[20,230],[100,255]], 밝기 +40, 원본의 오른쪽 열 crop. 별도 컬러 변환: BGR=(5,40,220).
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
brightened [[60, 255], [140, 255]]
right_column [[230], [255]]
BGR_to_RGB (220, 40, 5)
검증 방법
밝기 출력과 crop 출력의 원본이 같은지 확인한다. stdout의 230→255와 255→255가 정보 손실의 직접 증거다.
선택 코드 내부 assert는 RGB 첫값=(220,40,5), ROI shape=(2,1,3), 확대 shape=(8,8,3)을 검사한다. 이 assert의 통과는 실제 설치 후에만 증거가 된다.
흔한 오해와 실패 복구
파일이 None이면 모델 탓을 하지 말고 경로/파일형식부터 복구한다. 이번 합성 예제에서는 imwrite 결과도 확인한다.
흰 부분이 클리핑되면 원본을 보존하고 덧셈량을 줄인다. 변환 결과를 원본 이름으로 덮어쓰지 않는다.
OpenCV 창이 안 뜨는 headless 환경에서는 imshow를 추가하지 말고 PNG 저장 결과를 확인한다.
확인 문제
밝기 +40 뒤 서로 다른 230과 255가 둘 다 255가 되었다. 옳은 설명은?
자동채점 · 선택형
해설과 정답 보기
둘이 같은 결과로 합쳐졌기 때문에 역변환이 유일하지 않다.
정답: 클리핑으로 구별 정보가 사라짐
BGR=(5,40,220)의 RGB 값과 밝기 조절이 학습이 아닌 이유를 설명하라.
자기평가 · 자동채점 아님
평가 기준: 채널 역순값과 가중치 학습 없음 두 항목을 평가한다. 클리핑 손실 계산은 이 질문의 필수답이 아니다.
해설 보기
RGB는 (220,40,5). 밝기 조절은 정해 둔 산술 규칙이며 라벨과 손실로 가중치를 갱신하지 않는다.
독립 실습
독립 입력 [[0,250],[60,120]]에 +20을 적용하고 손실 칸을 표시하라. 이어 +20 대신 -30을 적용할 때 아래쪽 0 경계까지 보호하도록 코드를 고쳐라.
남길 증거: 두 독립 입력의 수정 코드·stdout·손실 위치. 선택했다면 PNG와 실제 패키지 버전; 미선택이면 라이브러리 미실행 표기.
기본 worked example — 표준라이브러리 실제 검산
image = [[20, 230], [100, 255]]
out = [[min(255, int(v) + 40) for v in row] for row in image]
crop = [row[1:2] for row in image]
assert out == [[60, 255], [140, 255]]
assert crop == [[230], [255]]
print('brightened', out)
print('right_column', crop)
print('BGR_to_RGB', (5, 40, 220)[::-1])
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""OpenCV 선택 경로. 설치/라이브러리 실제 실행은 제작 시 하지 않았다."""
from pathlib import Path
import cv2
import numpy as np
# 외부 사진 대신 직접 만든 2x2 BGR 숫자 영상이다.
bgr = np.array([[[5,40,220],[230,230,230]], [[100,100,100],[255,255,255]]], dtype=np.uint8)
assert cv2.imwrite('pixel-input.png', bgr)
loaded = cv2.imread('pixel-input.png', cv2.IMREAD_COLOR)
if loaded is None:
raise FileNotFoundError('pixel-input.png 읽기 실패')
rgb = cv2.cvtColor(loaded, cv2.COLOR_BGR2RGB)
roi = loaded[:, 1:2].copy()
bright = np.clip(loaded.astype(np.int16) + 40, 0, 255).astype(np.uint8)
# 크기 튜플은 (width,height); array shape은 (height,width,channels).
large = cv2.resize(bright, (8, 8), interpolation=cv2.INTER_NEAREST)
assert tuple(rgb[0,0]) == (220,40,5)
assert tuple(bright[0,1]) == (255,255,255)
assert roi.shape == (2,1,3) and large.shape == (8,8,3)
assert cv2.imwrite('pixel-bright.png', large)
print('input_shape', loaded.shape, 'dtype', str(loaded.dtype))
print('rgb_first', rgb[0,0].tolist(), 'roi_shape', roi.shape)
print('bright_top_right', bright[0,1].tolist(), 'output_shape', large.shape)
print('saved', Path('pixel-bright.png').name)
선택 설치/실패복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
+20은 [[20,255],[80,140]]. 250+20의 270이 255로 잘리므로 15 단계가 잘린다. -30은 max(0,min(255,v-30))로 [[0,220],[30,90]]. 위 경계만 처리하는 기존 함수를 그대로 쓰면 음수가 남는다.
평가 기준: 실행 평가: 두 결과를 모두 확인. 자기평가: 손실 칸과 int 승격→clip 이유를 설명. 복원 불가능한 값을 다시 더해 복구했다고 주장하면 재실습.
새 맥락에 적용하기
직접 만든 공연 안내 카드의 작은 글자를 OCR에 넣기 전 crop·RGB/회색·resize 변환 일지를 작성하라. 확대 후 글자가 생긴 것처럼 해석하지 말고 원본 대조 위치를 남긴다.
내 말로 설명하고 가르치기
학생은 “컵 밖으로 넘친 잉크” 비유로 클리핑을 설명하고, 강사는 -30 반례로 아래 경계 처리를 확인한다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
거의 모든 자리가 비어 있는 사진에서 항상 빈자리라고 답한 모델이 높은 정확도를 받았다. 실제 사용자는 점유 자리를 빈자리라고 안내받아 헛걸음을 한다.
점유를 양성으로 고정하고 혼동행렬·정밀도·재현율을 계산하며 촬영 그룹 누출을 막는다.
먼저 알아둘 것
vision-pixels: 입력 변환 기록
핵심 한 문장: 평가의 첫 줄은 양성의 뜻과 자료를 나눈 단위다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
경보기는 조용했던 시간을 맞힌 수보다 놓친 경보와 쓸데없는 경보를 따로 세어야 한다.
비유가 닿지 않는 곳: 어떤 오류가 더 비싼지는 사용 목적에 따라 다르며, 비유만으로 임계값이나 출시 기준을 정할 수 없다.
실제로 작동하는 원리
여기서 truth=1은 실제 점유, pred=1은 점유 예측이다. TP는 점유를 점유로, TN은 빈자리를 빈자리로, FP는 빈자리를 점유로, FN은 점유를 빈자리로 잘못 알린 수다. 정밀도 TP/(TP+FP)는 점유라고 말한 것 중 맞은 비율, 재현율 TP/(TP+FN)는 실제 점유 중 잡은 비율이다. 분모가 0이면 “정의되지 않음”으로 남기고 무조건 100%로 채우지 않는다.
정답 [1,0,0,1,1], 예측 [1,0,1,0,1]을 한 줄씩 짝지으면 TP2/TN1/FP1/FN1. 정확도는 3/5, 정밀도와 재현율은 각각 2/3이다. 단순히 정확도 60%라고만 말하면 놓침 1건의 사용자 영향을 숨긴다.
train은 가중치 학습, val은 모델/임계값 선택, test는 선택이 끝난 뒤 최종 평가용이다. 같은 자전거·촬영 세션의 인접 프레임을 무작위로 나누면 비슷한 배경을 외워 점수가 부풀 수 있다. session/site/day 같은 그룹을 먼저 나누고 증강은 train 안에서만 만든다. 그룹 교집합이 비었다고 모든 누출이 사라지는 것은 아니다. 복제 파일과 동일 대상의 다른 이름도 확인한다. 최종 시험을 반복 보며 튜닝했다면 더 이상 독립 최종 시험이 아니다.
따라 하기
양성=점유를 종이 맨 위에 적고 다섯 정답/예측 쌍에 TP/TN/FP/FN을 붙인다.
python3 vision-evaluate.py로 계산과 그룹 교집합 검사를 실행한다.
같은 데이터에서 임계값을 낮출 때 일반적으로 점유 예측이 늘어 FN은 줄 수 있지만 FP가 늘 수 있음을 설명한다. 이 코드는 점수값이 없어 임계값 튜닝을 실행한 것은 아니다.
평가표에 조건·표본수·양성 정의·오류 사례·val/test 역할을 반드시 함께 적는다.
입력 예제
기본 정답 [1,0,0,1,1], 예측 [1,0,1,0,1]. 촬영그룹 train={site-a-day1}, val={site-a-day2}, test={site-b-day3}.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
TP TN FP FN 2 1 1 1
accuracy precision recall 0.6 0.667 0.667
group_split disjoint
test를 보고 임계값을 변경한 뒤 같은 test 점수를 새 성능처럼 발표하지 않는다. 새 독립 자료를 확보하거나 개발평가로 명칭을 바꾼다.
확인 문제
실제로 빈자리를 점유라고 안내했다. 점유=양성일 때 어떤 셀인가?
자동채점 · 선택형
해설과 정답 보기
truth=0/pred=1이므로 FP다.
정답: FP
같은 촬영의 인접 프레임을 train/test 양쪽에 넣으면 어떤 위험이 있고 어떻게 나눌 것인가?
자기평가 · 자동채점 아님
평가 기준: 유사 장면 누출의 원인과 그룹 단위 분리 해결책을 모두 말하면 통과. 혼동행렬 숫자는 이 질문에서 요구하지 않는다.
해설 보기
배경·대상이 거의 같아 장면 기억을 일반화로 오인한다. 촬영 세션/장소/날짜 그룹을 나눈 후 train 안에서만 증강한다.
독립 실습
새 truth=[1,1,0,0], pred=[1,0,1,0]의 네 셀·정밀도·재현율을 코드로 구하라. 이어 모든 pred를 0으로 바꾸었을 때 정밀도의 0분모를 안전하게 처리하라.
남길 증거: 두 독립 입력의 계산 로그, 분모0 처리 코드, 촬영그룹 누출을 고친 전후 목록.
기본 worked example — 표준라이브러리 실제 검산
truth = [1, 0, 0, 1, 1]
pred = [1, 0, 1, 0, 1]
assert len(truth) == len(pred)
tp = sum(a == b == 1 for a, b in zip(truth, pred))
tn = sum(a == b == 0 for a, b in zip(truth, pred))
fp = sum(a == 0 and b == 1 for a, b in zip(truth, pred))
fn = sum(a == 1 and b == 0 for a, b in zip(truth, pred))
assert (tp, tn, fp, fn) == (2, 1, 1, 1)
print('TP TN FP FN', tp, tn, fp, fn)
print('accuracy precision recall', round((tp+tn)/len(truth), 3), round(tp/(tp+fp), 3), round(tp/(tp+fn), 3))
groups = {'train': {'site-a-day1'}, 'val': {'site-a-day2'}, 'test': {'site-b-day3'}}
assert not (groups['train'] & groups['val'] or groups['train'] & groups['test'] or groups['val'] & groups['test'])
print('group_split', 'disjoint')
실습 해설 보기
첫 입력은 TP/TN/FP/FN 모두1, 정밀도·재현율 각각 1/2. 모두0 예측은 TP0/TN2/FP0/FN2이며 정밀도 분모가 0이어서 None 또는 정의되지 않음, 재현율0이다.
평가 기준: 자동: 첫 네 셀과 비율, 두 번째 분모 예외가 일치. 자기평가: 실제 점유를 놓치는 FN의 안내 위험 설명. 분모0을 1로 바꾸어 성능을 숨기면 불통과.
새 맥락에 적용하기
박물관 전시품 결함 검토로 전이하여 양성을 “결함 후보”로 다시 정의하고 FP의 검토시간과 FN의 재검사 필요를 별도 표로 적는다.
내 말로 설명하고 가르치기
학생은 혼동행렬 한 셀을 일상 문장으로 바꾸고 강사는 양성을 “빈자리”로 뒤집어 같은 오류의 이름이 달라짐을 묻는다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
읽음과 숙련은 별개입니다.
참고 출처와 확인 범위
https://docs.ultralytics.com/modes/val/ — 반환된 Val 사용 예와 Arguments 표의 split, conf, iou(NMS), workers, metrics.box.map/map50 설명을 읽음. 현행 기본 설명이 YOLO26인 점을 확인; 본 예제는 YOLO11 고정 설계이며 실행 호환성은 미검증.
학습 단원
작은 창이 사진 위를 걸으며 배우는 CNN
해결할 문제와 목표
이미지를 숫자로 읽을 수 있지만 어떤 숫자 조합이 점유 상태와 관계있는지는 직접 규칙으로 모두 만들기 어렵다.
공유 커널·ReLU·pooling·선형 head·손실·역전파를 한 계산 흐름으로 연결하고, 선택 경로에서 작은 PyTorch CNN의 train/val/test를 완결한다.
먼저 알아둘 것
vision-evaluate: 학습/검증/시험 분리
핵심 한 문장: CNN은 같은 작은 계산창을 여러 위치에 쓰고, 손실이 줄도록 그 창의 숫자를 학습한다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
투명한 작은 도장을 사진 위 여러 곳에 찍어 모서리나 색 변화를 표시한 뒤 표시들을 모아 판단한다.
비유가 닿지 않는 곳: 도장은 사람이 고정하지만 CNN 커널은 학습으로 바뀐다. 모서리를 찾는 층이 항상 사람이 붙인 의미와 일대일로 대응하지 않는다.
실제로 작동하는 원리
입력 3×3에 커널 [[1,0],[0,-1]]을 stride1, padding0으로 적용하면 가능한 창이 2×2다. 왼쪽 위는 1-4=-3, 오른쪽 위는 2-1=1, 왼쪽 아래는 3-2=1, 오른쪽 아래는 4-5=-1이다. 딥러닝 Conv2d의 기본 연산은 커널을 뒤집지 않는 cross-correlation이며 관습적으로 합성곱이라고 부른다. 같은 커널을 네 위치에 공유하므로 위치마다 다른 가중치를 모두 만들 필요가 없다.
ReLU는 음수를0으로 만들고 max pooling은 해당 구역 최댓값을 남긴다. 본 예는 [[0,1],[1,0]]→1이다. 위치 세부정보가 줄어드는 만큼 이미지 분류에는 편리하지만 픽셀 경계가 필요한 분할에서는 복원이 필요하다. 채널은 서로 다른 특징지도다. PyTorch 입력은 보통 N,C,H,W이며 원본 H,W,C에서 축을 바꿔야 한다.
학습은 forward→손실→zero_grad→backward→step이다. 순수Python 코드의 마지막 부분은 이해용 선형 head 하나만 미분한다. 특징2, 가중치0.5, 목표0이면 출력1·MSE1이며 기울기4, 학습률0.1로 가중치0.1·손실0.04가 된다. 이것을 CNN 전체를 학습했다고 주장하지 않는다. 선택 코드의 nn.Conv2d 파라미터까지 optimizer에 넣고 CrossEntropyLoss로 역전파하는 경로가 실제 CNN 학습이다.
선택 자료의 empty/occupied는 초록/빨강 합성 사각형에 붙인 교육용 이름이다. 학습이 색만 이용해도 높은 점수가 나올 수 있으므로 이 결과는 자전거의 형상 인식이나 야간 점유 성능이 아니다.
따라 하기
python3 vision-learn.py를 실행하기 전 네 창의 곱셈합과 ReLU를 직접 적는다.
stdout의 conv/relu/max_pool과 head 손실 변화를 비교한다. 커널은 기본 코드에서 고정되어 있음을 표시한다.
선택: setup의 PyTorch/Pillow를 설치하고 prepare_assets.py, optional_models.py를 저장한다. python prepare_assets.py 후 python optional_models.py cnn을 실행한다.
선택 학습은 5 epochs의 train_loss/val_accuracy를 기록하고 val 최고 state_dict를 복원한 뒤 test 혼동행렬을 한 번 계산한다. cnn.pt와 cnn-report.json을 보존한다. 제작 시 이 경로는 미실행이다.
입력 예제
image=[[1,2,0],[3,4,1],[0,2,5]], kernel=[[1,0],[0,-1]], stride1/padding0. head용 별도 입력 feature=2, weight=.5, target=0, lr=.1.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
conv [[-3, 1], [1, -1]] relu [[0, 1], [1, 0]] max_pool 1
head_loss_before_after 1.0 0.04
head_weight_before_after 0.5 0.1
검증 방법
기본 네 창의 좌표를 추적하고 ReLU 이후 음수가 없는지 검사한다. pooling 결과1만 맞고 중간행렬을 설명하지 못하면 재실습한다.
선택 보고서는 epochs 5건, classes empty/occupied, test 혼동행렬 합이 test_n과 같은지 검사한다. 실제 val 점수가 오르지 않으면 실패를 그대로 남긴다.
흔한 오해와 실패 복구
CrossEntropyLoss에는 softmax된 확률이 아니라 raw logits와 정수 클래스 ID를 준다. shape/class_to_idx 불일치부터 고친다.
train accuracy만 높아지면 복잡한 모델을 더하기 전에 split·라벨·색상 지름길을 점검한다. 재학습으로 test에 맞추지 않는다.
메모리/설치 실패면 CPU와 배치4를 유지하고 다른 무거운 프로세스를 중단할 권한이 없다면 실행을 보류한다. 가짜 학습 로그를 쓰지 않는다.
확인 문제
커널을 같은 이미지의 여러 위치에 사용하는 이유와 맞는 설명은?
자동채점 · 선택형
해설과 정답 보기
공유 커널이 반복 지역 패턴을 계산한다. 이는 시험 정답을 입력한다는 뜻이 아니다.
정답: 지역 패턴 계산의 가중치를 공유하기 위해
기본 손계산과 선택 PyTorch 코드 중 어느 쪽에서 커널이 학습되며, 어떤 호출들이 그 차이를 만드는가?
자기평가 · 자동채점 아님
평가 기준: 고정 커널/학습 커널 구분, backward와 step의 역할을 모두 설명. loss 값만 외우면 재시도.
해설 보기
기본 커널은 고정이다. PyTorch에서 loss.backward()가 미분하고 optimizer.step()이 Conv2d를 포함한 학습 가능 파라미터를 갱신한다.
독립 실습
기본 image의 첫 값1을4로 바꿔 convolution·ReLU·max pool 결과를 예측한 후 실행하라. 별도로 head의 학습률을0으로 바꾸어 손실이 줄지 않는 반례를 만든다.
남길 증거: 네 창 계산표, 수정 실행 stdout, lr0 반례. 선택 시 cnn-report.json/환경 버전/학습하지 못한 조건.
기본 worked example — 표준라이브러리 실제 검산
image = [[1, 2, 0], [3, 4, 1], [0, 2, 5]]
kernel = [[1, 0], [0, -1]]
conv = [[sum(image[y+i][x+j]*kernel[i][j] for i in range(2) for j in range(2))
for x in range(2)] for y in range(2)]
relu = [[max(0, x) for x in row] for row in conv]
assert conv == [[-3, 1], [1, -1]] and relu == [[0, 1], [1, 0]]
print('conv', conv, 'relu', relu, 'max_pool', max(map(max, relu)))
# 최종 선형 head 하나의 MSE 미분 예시. 전체 CNN 역전파를 구현한 것이 아니다.
feature, weight, target, rate = 2.0, .5, 0.0, .1
prediction = feature * weight
grad = 2 * (prediction-target) * feature
new_weight = weight-rate*grad
print('head_loss_before_after', round((prediction-target)**2, 4), round((feature*new_weight-target)**2, 4))
print('head_weight_before_after', weight, round(new_weight, 4))
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 경로의 독자 합성 자료. Pillow 필요; 제작 시 미실행.
아무 이미지도 다운로드하지 않는다. 새 vision_data 폴더만 생성한다.
"""
import json
import random
from pathlib import Path
from PIL import Image, ImageDraw
root = Path('vision_data')
if root.exists():
raise SystemExit('vision_data가 이미 있습니다. 기존 증거를 보존하고 새 학습 폴더에서 실행하세요.')
root.mkdir()
manifest = []
for split, seed, count in [('train', 11, 40), ('val', 22, 12), ('test', 33, 12)]:
rng = random.Random(seed)
for i in range(count):
# 같은 원본에서 만든 증강판을 다른 split에 넣지 않는다.
# seed 분리는 독립 난수 실습일 뿐 실제 촬영일/장소 분리의 대체가 아니다.
for label, color in [('empty', (40, 190, 70)), ('occupied', (210, 60, 40))]:
im = Image.new('RGB', (64, 64), tuple(rng.randint(15, 35) for _ in range(3)))
d = ImageDraw.Draw(im)
x, y = rng.randint(6, 20), rng.randint(6, 20)
d.rectangle((x, y, x + 30, y + 30), fill=color)
dest = root / 'classify' / split / label / f'{split}-{i:03}.png'
dest.parent.mkdir(parents=True, exist_ok=True)
im.save(dest)
# 탐지 target=표식; 자전거 사전학습 성능으로 오인하지 않는다.
im = Image.new('RGB', (64, 64), (22, 22, 22))
mask = Image.new('L', (64, 64), 0)
d, md = ImageDraw.Draw(im), ImageDraw.Draw(mask)
x1, y1 = rng.randint(4, 26), rng.randint(4, 26)
width, height = rng.randint(16, 26), rng.randint(16, 26)
x2, y2 = x1 + width, y1 + height
# PIL rectangle의 끝점은 포함됨: 연속좌표 상자 x2/y2와 일치시키기 위해 -1.
d.rectangle((x1, y1, x2 - 1, y2 - 1), fill=(40, 190, 70))
md.rectangle((x1, y1, x2 - 1, y2 - 1), fill=1)
for kind, obj in [('images', im), ('masks', mask)]:
path = root / 'segment' / split / kind / f'{split}-{i:03}.png'
path.parent.mkdir(parents=True, exist_ok=True)
obj.save(path)
imgp = root / 'detect' / 'images' / split / f'{split}-{i:03}.png'
labp = root / 'detect' / 'labels' / split / f'{split}-{i:03}.txt'
imgp.parent.mkdir(parents=True, exist_ok=True)
labp.parent.mkdir(parents=True, exist_ok=True)
im.save(imgp)
vals = ((x1 + x2) / 128, (y1 + y2) / 128, width / 64, height / 64)
labp.write_text('0 ' + ' '.join(f'{v:.6f}' for v in vals) + '\n', encoding='utf-8')
manifest.append({'split': split, 'group': f'{split}-{i:03}', 'box_xyxy': [x1,y1,x2,y2]})
(root / 'detect' / 'data.yaml').write_text(
'path: ' + json.dumps(str((root / 'detect').resolve())) + '\n'
'train: images/train\nval: images/val\ntest: images/test\nnames:\n 0: marker\n', encoding='utf-8')
(root / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
print('created synthetic vision_data; counts per detect split: train=40 val=12 test=12')
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 실행 전용. 이번 원고 제작에서는 import/학습/다운로드하지 않음.
Python 3.11+의 별도 가상환경에서 실행. 모든 출력은 현재 학습 폴더에만 저장.
"""
import argparse
import copy
import json
from pathlib import Path
def classification(transfer=False, allow_download=False):
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, models, transforms
torch.manual_seed(73)
torch.set_num_threads(2)
weights = models.ResNet18_Weights.IMAGENET1K_V1 if transfer else None
if transfer and not allow_download:
raise SystemExit('전이학습 가중치 다운로드/라이선스 검토 후 --allow-download 필요')
transform = weights.transforms() if transfer else transforms.Compose([
transforms.Resize((64, 64)), transforms.ToTensor()])
data = {s: datasets.ImageFolder(Path('vision_data/classify') / s, transform)
for s in ('train', 'val', 'test')}
assert all(d.class_to_idx == data['train'].class_to_idx for d in data.values())
assert all(len(d) > 0 for d in data.values())
nclass = len(data['train'].classes)
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train'), num_workers=0)
for s, d in data.items()}
if transfer:
model = models.resnet18(weights=weights)
for p in model.parameters():
p.requires_grad_(False)
model.fc = nn.Linear(model.fc.in_features, nclass)
else:
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), nn.Conv2d(8, 16, 3, padding=1),
nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(16, nclass))
optimizer = torch.optim.Adam((p for p in model.parameters() if p.requires_grad), lr=0.003)
loss_fn = nn.CrossEntropyLoss()
frozen_before = {k: v.clone() for k, v in model.state_dict().items()
if transfer and not k.startswith('fc.')}
best_score, best_state = -1, None
log = []
for epoch in range(5):
# 동결 backbone의 BatchNorm running statistics까지 유지한다.
if transfer:
model.eval()
model.fc.train()
else:
model.train()
total_loss = 0.0
for x, y in loaders['train']:
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
model.eval()
with torch.inference_mode():
correct = sum((model(x).argmax(1) == y).sum().item() for x, y in loaders['val'])
val_accuracy = correct / len(data['val'])
log.append({'epoch': epoch + 1, 'train_loss': total_loss / len(data['train']),
'val_accuracy': val_accuracy})
if val_accuracy > best_score:
best_score, best_state = val_accuracy, copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
model.eval()
matrix = [[0] * nclass for _ in range(nclass)]
with torch.inference_mode():
for x, y in loaders['test']:
for truth, pred in zip(y.tolist(), model(x).argmax(1).tolist()):
matrix[truth][pred] += 1
if transfer:
assert all(torch.equal(v, model.state_dict()[k]) for k, v in frozen_before.items())
prefix = 'transfer' if transfer else 'cnn'
torch.save({'state_dict': best_state, 'classes': data['train'].classes}, prefix + '.pt')
report = {'mode': prefix, 'classes': data['train'].classes, 'epochs': log,
'test_confusion_rows_truth': matrix, 'test_n': len(data['test']),
'scope': 'synthetic colored plates only; NOT bicycle deployment'}
Path(prefix + '-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def semantic_mask():
import numpy as np
from PIL import Image
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
torch.manual_seed(73)
torch.set_num_threads(2)
class Pairs(Dataset):
def __init__(self, split):
self.files = sorted((Path('vision_data/segment') / split / 'images').glob('*.png'))
assert self.files, '먼저 prepare_assets.py 실행'
def __len__(self):
return len(self.files)
def __getitem__(self, i):
p = self.files[i]
x = np.array(Image.open(p).convert('RGB'), dtype=np.float32) / 255
m = np.array(Image.open(p.parent.parent / 'masks' / p.name), dtype=np.int64)
assert m.shape == x.shape[:2] and set(np.unique(m)).issubset({0, 1}), 'mask shape/labels invalid'
return torch.from_numpy(x).permute(2, 0, 1), torch.from_numpy(m)
sets = {s: Pairs(s) for s in ('train', 'val', 'test')}
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train')) for s, d in sets.items()}
# 배경0/합성 표식1: 각 픽셀의 두 logit을 학습한다. 사전학습 아님.
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.Conv2d(8, 2, 1))
opt = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
def score(split):
inter = union = 0
model.eval()
with torch.inference_mode():
for x, y in loaders[split]:
pred = model(x).argmax(1)
inter += ((pred == 1) & (y == 1)).sum().item()
union += ((pred == 1) | (y == 1)).sum().item()
return inter / union if union else None
best, state = -1, None
for epoch in range(8):
model.train()
for x, y in loaders['train']:
opt.zero_grad()
loss = criterion(model(x), y)
loss.backward()
opt.step()
val_iou = score('val')
print('epoch', epoch + 1, 'val_iou', val_iou)
if val_iou is not None and val_iou > best:
best, state = val_iou, copy.deepcopy(model.state_dict())
assert state is not None
model.load_state_dict(state)
test_iou = score('test')
x, truth = sets['test'][0]
with torch.inference_mode():
pred = model(x.unsqueeze(0)).argmax(1)[0].numpy().astype('uint8')
Image.fromarray(pred * 255).save('mask-prediction.png')
Image.fromarray(truth.numpy().astype('uint8') * 255).save('mask-truth.png')
torch.save(state, 'mask-model.pt')
print(json.dumps({'test_foreground_iou': test_iou, 'test_n': len(sets['test'])}))
# 연결요소는 붙은 물체를 분리하지 못한다. 학습된 인스턴스 분할 모델이 아니다.
import cv2
n, instance_ids = cv2.connectedComponents(pred, connectivity=4)
np.save('mask-component-ids.npy', instance_ids)
print('connected_components_not_true_instances', n - 1)
def anomaly():
import numpy as np
from PIL import Image
from sklearn.ensemble import IsolationForest
rng = np.random.default_rng(73)
def image(defect=False, dark=False):
a = rng.normal(120, 3, (32, 32))
if defect:
a[12:18, 12:18] = 220
if dark:
a -= 40
return np.clip(a, 0, 255).astype(np.uint8)
def feature(a):
return [float(a.mean()), float(a.std()), float(np.quantile(a, .99))]
# 정상만 fit; 독립 정상 val로 threshold; test를 threshold 설정에 사용하지 않는다.
train = [image() for _ in range(80)]
val = [image() for _ in range(30)]
model = IsolationForest(n_estimators=80, contamination='auto', random_state=73, n_jobs=1)
model.fit([feature(a) for a in train])
threshold = float(np.quantile(-model.score_samples([feature(a) for a in val]), .95))
tests = [('normal', image()), ('bright-patch', image(defect=True)), ('dark-normal', image(dark=True))]
report = []
for name, a in tests:
score = float(-model.score_samples([feature(a)])[0])
report.append({'case': name, 'score': score, 'threshold': threshold,
'review_candidate': score > threshold})
Image.fromarray(a).save('anomaly-' + name + '.png')
Path('anomaly-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
# 통계적 변화 후보일 뿐 결함 종류/의료 진단/설비 정지 명령이 아니다.
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['cnn', 'transfer', 'mask', 'anomaly'])
p.add_argument('--allow-download', action='store_true')
a = p.parse_args()
if a.mode == 'cnn':
classification()
elif a.mode == 'transfer':
classification(True, a.allow_download)
elif a.mode == 'mask':
semantic_mask()
else:
anomaly()
if __name__ == '__main__':
main()
CNN 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
첫 창만 -3에서0으로 바뀌므로 conv=[[0,1],[1,-1]], ReLU는 기존과 같고 pool1이다. head lr0이면 weight0.5/손실1이 그대로다. 관측 출력이 같아도 중간 특징이 다를 수 있다.
평가 기준: 자동: 수정 conv와 lr0 손실을 재현. 자기평가: 커널 공유와 gradient update 두 원리 설명. 선택 실행 여부는 별도 기록하며 실제 모델 성공을 필수 기본 통과로 꾸미지 않는다.
새 맥락에 적용하기
비민감 합성 포장 상자 분류에 옮겨 배경색만으로 라벨을 맞추는 shortcut을 만들어 보고, 배경색을 바꾼 새 그룹에서 평가 설계를 제출한다.
내 말로 설명하고 가르치기
학생이 종이에 작은 창 네 번을 직접 움직여 보인다. 강사는 stride2 또는 padding을 추가하면 창 개수가 달라지는 이유를 묻고, 학습과 고정 필터의 차이를 다시 확인한다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
핵심 한 문장: 전이학습은 배운 가중치를 출발점으로 새 목표를 학습하는 것이지, 파일을 받기만 하는 일이 아니다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
그림을 볼 줄 아는 관찰자에게 새로운 분류 이름표를 가르치는 것부터 시작한다.
비유가 닿지 않는 곳: 관찰자가 사람처럼 개념을 이해한다는 뜻은 아니다. 원래 훈련 분포와 다른 조명·형태에서는 기존 특징 자체가 부족할 수 있다.
실제로 작동하는 원리
사전학습 모델은 다른 데이터의 손실을 줄여 얻은 가중치다. 그대로 이미지 한 장을 넣으면 사전학습 추론이다. 기존 feature extractor를 동결하고 마지막 분류층만 새 범주 수에 맞춰 학습하면 고정 특징 기반 전이학습이다. 일부 또는 전체 backbone까지 작은 학습률로 바꾸면 fine-tuning이다. 새 head 학습만으로 부족한지 val에서 확인한 다음 동결 해제를 검토한다.
순수Python 예제는 이미 얻었다고 가정한 특징 [1,0],[0,1]과 목표 [1,0]을 사용한다. head=[.2,.8], 평균MSE=.64. 기울기 [-.8,.8]을 학습률.1로 적용하면 [.28,.72], 평균MSE=.5184다. 특징 값은 그대로라는 assert가 동결 원리를 검증한다. 이 특징은 실제 ResNet에서 뽑은 값이 아니며 작은 수치 모형이다.
선택 경로는 torchvision ResNet18의 IMAGENET1K_V1 가중치와 그 weights.transforms()를 함께 쓴다. RGB·크기·정규화가 가중치 기대와 맞아야 한다. 모든 backbone parameter의 requires_grad=False, 새 fc만 optimizer에 넣는다. BatchNorm은 parameter를 동결해도 model.train()에서 running mean/variance를 바꿀 수 있으므로 backbone을 eval에 두고 fc만 train으로 둔다. 전체 state_dict에서 fc 외 값이 그대로인지 비교하는 이유다.
val 최고 모델을 선택한 뒤 test를 한 번 읽는다. 원래 1000개 ImageNet 클래스 head를 그대로 두고 empty/occupied라고 임의 해석하지 않는다. 저장하는 class_to_idx와 변환을 배포 추론에도 보존해야 한다.
따라 하기
python3 vision-transfer.py를 실행하여 특징 불변과 head 손실 감소를 확인한다.
CNN 단원의 선택 자료를 같은 폴더에 저장하되 데이터 생성은 한 번만 한다. setup의 PyTorch 환경을 준비한다.
공개 가중치 사용권/공간/다운로드 허용을 검토한 후에만 python optional_models.py transfer --allow-download를 실행한다. 가중치 다운로드·실학습은 제작 시 미실행이다.
transfer-report.json의 val 추이·test 혼동행렬을 CNN 경로와 비교하되 데이터가 너무 단순하므로 일반적인 전이학습 우수성의 증거로 쓰지 않는다.
입력 예제
feature=[[1,0],[0,1]], target=[1,0], head=[.2,.8], 학습률 .1. 실제 제품 선택은 별도 합성 색상 이미지와 ResNet18.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
head_weights [0.28, 0.72]
mean_MSE_before_after 0.64 0.5184 features_unchanged True
검증 방법
features_unchanged=True와 손실 .64→.5184를 확인한다. 이것은 모의 특징값에 한정한다.
선택 코드의 frozen_before 비교 assert가 BatchNorm buffer까지 같음을 확인한다. train/val/test class_to_idx 일치도 검사한다.
흔한 오해와 실패 복구
requires_grad=False만 쓰고 backbone.train()을 호출하면 BatchNorm 통계가 바뀔 수 있다. eval+fc.train으로 복구한다.
가중치 다운로드 실패를 weights=None으로 조용히 대체하면 전이학습이 아니라 scratch 학습이다. 중단하고 미실행으로 기록한다.
val/test의 데이터 증강을 train과 같이 무작위로 적용하면 평가가 흔들린다. 본 선택 코드는 가중치의 고정 transforms를 쓴다.
확인 문제
가중치를 받고 기존 head로 이미지 한 장을 처리했다. 정확한 명칭은?
자동채점 · 선택형
해설과 정답 보기
새 라벨의 손실로 가중치를 갱신하지 않았으므로 추론이다.
정답: 사전학습 모델 추론
parameter 동결만 했는데 BatchNorm 값이 바뀔 수 있는 이유와 이 교재 코드의 해결책을 설명하라.
자기평가 · 자동채점 아님
평가 기준: buffer/parameter 차이, 모드 분리, 비교 검증을 말하면 통과. 임의 정확도 수치는 요구하지 않는다.
해설 보기
running statistics는 gradient parameter가 아닌 buffer이며 train 모드에서 갱신될 수 있다. backbone eval과 새 fc train을 나누고 state_dict를 비교한다.
독립 실습
모의 head 학습률을 .5로 바꾸고 특징은 그대로 둔 채 갱신 head·손실을 구하라. 그다음 “새 야간 조건에 실패했다”는 상황에서 head 유지/부분 fine-tuning/데이터 추가 중 무엇을 먼저 확인할지 근거를 쓴다.
남길 증거: 수정 head 계산 stdout, 세 학습 상태의 구분표. 선택 시 다운로드 여부·동결 검사·test 보고서와 미검증 한계.
기본 worked example — 표준라이브러리 실제 검산
features = [[1.0, 0.0], [0.0, 1.0]]
labels = [1.0, 0.0]
frozen_before = [r[:] for r in features]
w = [.2, .8]
rate = .1
before = sum((sum(a*b for a,b in zip(x,w))-y)**2 for x,y in zip(features,labels))/2
grads = [sum((sum(a*b for a,b in zip(x,w))-y)*x[j] for x,y in zip(features,labels)) for j in range(2)]
w = [v-rate*g for v,g in zip(w,grads)]
after = sum((sum(a*b for a,b in zip(x,w))-y)**2 for x,y in zip(features,labels))/2
assert features == frozen_before and after < before
print('head_weights', [round(v, 3) for v in w])
print('mean_MSE_before_after', round(before, 4), round(after, 4), 'features_unchanged', features == frozen_before)
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 경로의 독자 합성 자료. Pillow 필요; 제작 시 미실행.
아무 이미지도 다운로드하지 않는다. 새 vision_data 폴더만 생성한다.
"""
import json
import random
from pathlib import Path
from PIL import Image, ImageDraw
root = Path('vision_data')
if root.exists():
raise SystemExit('vision_data가 이미 있습니다. 기존 증거를 보존하고 새 학습 폴더에서 실행하세요.')
root.mkdir()
manifest = []
for split, seed, count in [('train', 11, 40), ('val', 22, 12), ('test', 33, 12)]:
rng = random.Random(seed)
for i in range(count):
# 같은 원본에서 만든 증강판을 다른 split에 넣지 않는다.
# seed 분리는 독립 난수 실습일 뿐 실제 촬영일/장소 분리의 대체가 아니다.
for label, color in [('empty', (40, 190, 70)), ('occupied', (210, 60, 40))]:
im = Image.new('RGB', (64, 64), tuple(rng.randint(15, 35) for _ in range(3)))
d = ImageDraw.Draw(im)
x, y = rng.randint(6, 20), rng.randint(6, 20)
d.rectangle((x, y, x + 30, y + 30), fill=color)
dest = root / 'classify' / split / label / f'{split}-{i:03}.png'
dest.parent.mkdir(parents=True, exist_ok=True)
im.save(dest)
# 탐지 target=표식; 자전거 사전학습 성능으로 오인하지 않는다.
im = Image.new('RGB', (64, 64), (22, 22, 22))
mask = Image.new('L', (64, 64), 0)
d, md = ImageDraw.Draw(im), ImageDraw.Draw(mask)
x1, y1 = rng.randint(4, 26), rng.randint(4, 26)
width, height = rng.randint(16, 26), rng.randint(16, 26)
x2, y2 = x1 + width, y1 + height
# PIL rectangle의 끝점은 포함됨: 연속좌표 상자 x2/y2와 일치시키기 위해 -1.
d.rectangle((x1, y1, x2 - 1, y2 - 1), fill=(40, 190, 70))
md.rectangle((x1, y1, x2 - 1, y2 - 1), fill=1)
for kind, obj in [('images', im), ('masks', mask)]:
path = root / 'segment' / split / kind / f'{split}-{i:03}.png'
path.parent.mkdir(parents=True, exist_ok=True)
obj.save(path)
imgp = root / 'detect' / 'images' / split / f'{split}-{i:03}.png'
labp = root / 'detect' / 'labels' / split / f'{split}-{i:03}.txt'
imgp.parent.mkdir(parents=True, exist_ok=True)
labp.parent.mkdir(parents=True, exist_ok=True)
im.save(imgp)
vals = ((x1 + x2) / 128, (y1 + y2) / 128, width / 64, height / 64)
labp.write_text('0 ' + ' '.join(f'{v:.6f}' for v in vals) + '\n', encoding='utf-8')
manifest.append({'split': split, 'group': f'{split}-{i:03}', 'box_xyxy': [x1,y1,x2,y2]})
(root / 'detect' / 'data.yaml').write_text(
'path: ' + json.dumps(str((root / 'detect').resolve())) + '\n'
'train: images/train\nval: images/val\ntest: images/test\nnames:\n 0: marker\n', encoding='utf-8')
(root / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
print('created synthetic vision_data; counts per detect split: train=40 val=12 test=12')
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 실행 전용. 이번 원고 제작에서는 import/학습/다운로드하지 않음.
Python 3.11+의 별도 가상환경에서 실행. 모든 출력은 현재 학습 폴더에만 저장.
"""
import argparse
import copy
import json
from pathlib import Path
def classification(transfer=False, allow_download=False):
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, models, transforms
torch.manual_seed(73)
torch.set_num_threads(2)
weights = models.ResNet18_Weights.IMAGENET1K_V1 if transfer else None
if transfer and not allow_download:
raise SystemExit('전이학습 가중치 다운로드/라이선스 검토 후 --allow-download 필요')
transform = weights.transforms() if transfer else transforms.Compose([
transforms.Resize((64, 64)), transforms.ToTensor()])
data = {s: datasets.ImageFolder(Path('vision_data/classify') / s, transform)
for s in ('train', 'val', 'test')}
assert all(d.class_to_idx == data['train'].class_to_idx for d in data.values())
assert all(len(d) > 0 for d in data.values())
nclass = len(data['train'].classes)
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train'), num_workers=0)
for s, d in data.items()}
if transfer:
model = models.resnet18(weights=weights)
for p in model.parameters():
p.requires_grad_(False)
model.fc = nn.Linear(model.fc.in_features, nclass)
else:
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), nn.Conv2d(8, 16, 3, padding=1),
nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(16, nclass))
optimizer = torch.optim.Adam((p for p in model.parameters() if p.requires_grad), lr=0.003)
loss_fn = nn.CrossEntropyLoss()
frozen_before = {k: v.clone() for k, v in model.state_dict().items()
if transfer and not k.startswith('fc.')}
best_score, best_state = -1, None
log = []
for epoch in range(5):
# 동결 backbone의 BatchNorm running statistics까지 유지한다.
if transfer:
model.eval()
model.fc.train()
else:
model.train()
total_loss = 0.0
for x, y in loaders['train']:
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
model.eval()
with torch.inference_mode():
correct = sum((model(x).argmax(1) == y).sum().item() for x, y in loaders['val'])
val_accuracy = correct / len(data['val'])
log.append({'epoch': epoch + 1, 'train_loss': total_loss / len(data['train']),
'val_accuracy': val_accuracy})
if val_accuracy > best_score:
best_score, best_state = val_accuracy, copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
model.eval()
matrix = [[0] * nclass for _ in range(nclass)]
with torch.inference_mode():
for x, y in loaders['test']:
for truth, pred in zip(y.tolist(), model(x).argmax(1).tolist()):
matrix[truth][pred] += 1
if transfer:
assert all(torch.equal(v, model.state_dict()[k]) for k, v in frozen_before.items())
prefix = 'transfer' if transfer else 'cnn'
torch.save({'state_dict': best_state, 'classes': data['train'].classes}, prefix + '.pt')
report = {'mode': prefix, 'classes': data['train'].classes, 'epochs': log,
'test_confusion_rows_truth': matrix, 'test_n': len(data['test']),
'scope': 'synthetic colored plates only; NOT bicycle deployment'}
Path(prefix + '-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def semantic_mask():
import numpy as np
from PIL import Image
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
torch.manual_seed(73)
torch.set_num_threads(2)
class Pairs(Dataset):
def __init__(self, split):
self.files = sorted((Path('vision_data/segment') / split / 'images').glob('*.png'))
assert self.files, '먼저 prepare_assets.py 실행'
def __len__(self):
return len(self.files)
def __getitem__(self, i):
p = self.files[i]
x = np.array(Image.open(p).convert('RGB'), dtype=np.float32) / 255
m = np.array(Image.open(p.parent.parent / 'masks' / p.name), dtype=np.int64)
assert m.shape == x.shape[:2] and set(np.unique(m)).issubset({0, 1}), 'mask shape/labels invalid'
return torch.from_numpy(x).permute(2, 0, 1), torch.from_numpy(m)
sets = {s: Pairs(s) for s in ('train', 'val', 'test')}
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train')) for s, d in sets.items()}
# 배경0/합성 표식1: 각 픽셀의 두 logit을 학습한다. 사전학습 아님.
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.Conv2d(8, 2, 1))
opt = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
def score(split):
inter = union = 0
model.eval()
with torch.inference_mode():
for x, y in loaders[split]:
pred = model(x).argmax(1)
inter += ((pred == 1) & (y == 1)).sum().item()
union += ((pred == 1) | (y == 1)).sum().item()
return inter / union if union else None
best, state = -1, None
for epoch in range(8):
model.train()
for x, y in loaders['train']:
opt.zero_grad()
loss = criterion(model(x), y)
loss.backward()
opt.step()
val_iou = score('val')
print('epoch', epoch + 1, 'val_iou', val_iou)
if val_iou is not None and val_iou > best:
best, state = val_iou, copy.deepcopy(model.state_dict())
assert state is not None
model.load_state_dict(state)
test_iou = score('test')
x, truth = sets['test'][0]
with torch.inference_mode():
pred = model(x.unsqueeze(0)).argmax(1)[0].numpy().astype('uint8')
Image.fromarray(pred * 255).save('mask-prediction.png')
Image.fromarray(truth.numpy().astype('uint8') * 255).save('mask-truth.png')
torch.save(state, 'mask-model.pt')
print(json.dumps({'test_foreground_iou': test_iou, 'test_n': len(sets['test'])}))
# 연결요소는 붙은 물체를 분리하지 못한다. 학습된 인스턴스 분할 모델이 아니다.
import cv2
n, instance_ids = cv2.connectedComponents(pred, connectivity=4)
np.save('mask-component-ids.npy', instance_ids)
print('connected_components_not_true_instances', n - 1)
def anomaly():
import numpy as np
from PIL import Image
from sklearn.ensemble import IsolationForest
rng = np.random.default_rng(73)
def image(defect=False, dark=False):
a = rng.normal(120, 3, (32, 32))
if defect:
a[12:18, 12:18] = 220
if dark:
a -= 40
return np.clip(a, 0, 255).astype(np.uint8)
def feature(a):
return [float(a.mean()), float(a.std()), float(np.quantile(a, .99))]
# 정상만 fit; 독립 정상 val로 threshold; test를 threshold 설정에 사용하지 않는다.
train = [image() for _ in range(80)]
val = [image() for _ in range(30)]
model = IsolationForest(n_estimators=80, contamination='auto', random_state=73, n_jobs=1)
model.fit([feature(a) for a in train])
threshold = float(np.quantile(-model.score_samples([feature(a) for a in val]), .95))
tests = [('normal', image()), ('bright-patch', image(defect=True)), ('dark-normal', image(dark=True))]
report = []
for name, a in tests:
score = float(-model.score_samples([feature(a)])[0])
report.append({'case': name, 'score': score, 'threshold': threshold,
'review_candidate': score > threshold})
Image.fromarray(a).save('anomaly-' + name + '.png')
Path('anomaly-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
# 통계적 변화 후보일 뿐 결함 종류/의료 진단/설비 정지 명령이 아니다.
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['cnn', 'transfer', 'mask', 'anomaly'])
p.add_argument('--allow-download', action='store_true')
a = p.parse_args()
if a.mode == 'cnn':
classification()
elif a.mode == 'transfer':
classification(True, a.allow_download)
elif a.mode == 'mask':
semantic_mask()
else:
anomaly()
if __name__ == '__main__':
main()
전이학습 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
head=[.6,.4], 평균MSE=.16이며 특징 불변이다. 야간 실패는 우선 전처리·라벨·조명 분포를 점검하고 val로 기준선을 만든다. head만의 한계가 관측되고 데이터가 확보되었을 때 backbone 일부를 작은 학습률로 해제한다. 작은 모형의 개선값은 실모델 효과가 아니다.
평가 기준: 자동: .5 학습률의 head와 손실/특징 불변 재현. 자기평가: 야간 실패에 데이터를 보지 않고 무조건 fine-tuning을 처방하지 않고 val/test 분리를 유지한다.
새 맥락에 적용하기
도서관 책 대신 직접 그린 공구 표식 분류로 전이한다. 특징추출과 라벨 사전을 어떻게 재사용하고, 어떤 데이터와 head를 새로 준비해야 하는지 명세한다.
내 말로 설명하고 가르치기
학생은 “눈은 고정, 이름표만 연습”을 설명하고 강사는 “눈의 통계가 은근히 바뀐다면?”이라고 물어 BatchNorm까지 답하게 한다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
한 자전거를 두 개의 상자로 검출한 결과를 두 대로 세었다. 라벨 좌표가 픽셀인지 정규화 값인지도 섞여 학습이 불안정하다.
YOLO 구조 역할·커스텀 라벨 변환·일대일 IoU 매칭·학습/평가 경로를 구현하고 지표 임계값 세 종류를 구별한다.
먼저 알아둘 것
vision-transfer: 모델 초기화와 학습 구분
핵심 한 문장: 네모 후보를 만드는 것, 중복을 줄이는 것, 정답과 맞추는 것은 서로 다른 단계다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
여러 사람이 같은 가게에 붙인 지도 핀은 같은 가게라고 한 번만 채점해야 한다.
비유가 닿지 않는 곳: 겹친 핀을 하나 지우는 규칙은 가려져 붙어 있는 서로 다른 물체를 지울 수도 있다.
실제로 작동하는 원리
YOLO 계열은 한 forward에서 다중 스케일 특징으로 물체의 클래스와 위치 후보를 낸다. backbone은 특징을 추출하고 neck은 해상도가 다른 특징을 결합하며 head는 상자와 클래스 출력을 만든다. 모든 YOLO 버전이 같은 head/NMS 구조인 것은 아니다. 이 선택 실습은 명시적으로 YOLO11n을 사용하고 최신 모델 일반론과 섞지 않는다.
라벨은 이미지당 txt, 한 물체당 class xc yc w h. 가로200·세로100에서 (20,10,100,50)의 중심은(60,30), 크기(80,40)이므로 0 .3 .3 .4 .4다. 정규화 분모는 x/w에 이미지 너비, y/h에 이미지 높이다. 예제 상자는 연속좌표의 x2/y2 끝점이며 면적에 +1을 하지 않는다. 부분 가림은 보이는 범위를 라벨링할지 추정 전체를 라벨링할지 정하고 검수자가 같은 정책을 쓰게 한다. 이 합성 데이터에는 가림이 없어 그 정책을 학습한 것으로 주장할 수 없다.
IoU는 교집합/합집합이다. (0,0,2,2)와(0,0,1,2)는 .5. 점수순 일대일 매칭에서 정답 하나를 두 번 TP로 쓸 수 없다. 동일 정답을 가리키는 예측 두 개는 하나 TP, 남은 하나 FP, 누락0이다. 실제 채점은 클래스도 일치해야 한다. 본 기본 코드는 단일 class 가정이다.
conf는 낮은 점수 후보를 버리는 기준, NMS의 iou는 중복을 억제하는 기준, 평가 IoU .5/.5:.95는 정답 매칭 기준이다. Ultralytics val(iou=.7)은 평가 매칭 기준을 .7로 바꾸는 코드가 아니라 NMS 설정이다. mAP50은 IoU.5에서 점수순 정밀도-재현율 곡선 아래 면적을 클래스별 평균한 값, mAP50-95는 여러 매칭 IoU에서 평균한 값이다. 한 임계값의 precision/recall과 mAP를 같은 수치로 기대하지 않는다.
선택: setup의 Ultralytics 환경에서 prepare_assets.py로 독자 사각형 이미지·YOLO txt·data.yaml을 만든다. train/val/test 폴더와 라벨 파일명을 열어 대응을 확인한다.
python optional_yolo.py train을 실행한다. YAML 구조로 무작위 초기화하고 pretrained=False, amp=False, CPU, 3 epochs를 명시해 사전학습 다운로드를 의도하지 않는 경로로 둔다. 라이브러리 설치 자체는 네트워크를 쓸 수 있다.
출력한 실제 best.pt 경로와 yolo-report.json을 기록한다. 3 epochs 합성 데모는 성능 보장이 아니다. mAP=0도 정상적으로 기록할 결과다. 선택 학습·평가는 제작 시 미실행이다.
입력 예제
가로200·세로100, class0 상자(20,10,100,50). 일대일 평가 정답=[(0,0,2,2)], 예측=[(.9,(0,0,2,2)),(.8,(0,0,1,2))], 매칭IoU≥.5.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
yolo_label 0 0.3 0.3 0.4 0.4
IoU_half 0.5
one_to_one_TP_FP_FN 1 1 0
invalid_box rejected
검증 방법
기본 출력의 TP1/FP1/FN0와 invalid_box rejected를 확인한다. 두 예측을 모두 TP로 세면 평가기가 틀렸다.
선택 학습 전 검사에서 라벨의 중심뿐 아니라 x±w/2, y±h/2가 범위 안인지 확인한다. 검증 폴더가 비어 있으면 중단해야 한다.
선택 보고서에 모델명·라이브러리 버전·split=test·conf=.001·NMS iou=.7·mAP 두 값이 있어야 한다. 실제 값은 실행하기 전 알 수 없다.
흔한 오해와 실패 복구
픽셀 상자를 정규화 없이 txt에 쓰면 대부분 값이1보다 커진다. 원 이미지 크기로 다시 변환한 후 이미지 위 라벨을 확인한다.
AGPL-3.0과 Enterprise 선택지가 있다는 사실만 확인했으며 상용 적합성을 보증하지 않는다. 배포 전 라이선스 검토 없이 제품에 넣지 않는다.
runs 폴더 이름이 자동 증가할 수 있으므로 README의 예시 best.pt 경로를 맹목적으로 쓰지 말고 학습 출력 경로를 사용한다.
정답 하나와 같은 클래스 예측 두 개가 모두 IoU 기준을 통과할 때 TP를 두 번 줄 수 없는 이유를 설명하라.
자기평가 · 자동채점 아님
평가 기준: 정답 사용중 상태와 중복 FP를 설명하면 통과. 라벨 정규화 산술은 이 질문의 필수답이 아니다.
해설 보기
정답 한 개를 중복 검출한 것이다. 점수순 일대일 매칭에서는 먼저 하나가 TP, 이미 사용된 정답을 가리키는 나머지는 FP다.
독립 실습
가로200·세로100에서 상자를 (100,20,180,80)으로 바꾸어 YOLO 라벨을 계산하라. 원래 정답 하나에 예측을 하나만 남기되 상자를 (1,0,3,2)로 바꾸어 IoU≥.5에서 TP/FP/FN을 구하라.
남길 증거: 새 라벨 한 줄, 일대일 매칭 계산, 불량 좌표 거부 로그. 선택 시 실제 best.pt 경로·학습/평가 보고서.
기본 worked example — 표준라이브러리 실제 검산
def iou(a, b):
for r in (a,b):
if not (r[0] < r[2] and r[1] < r[3]):
raise ValueError('invalid xyxy')
inter = max(0, min(a[2], b[2])-max(a[0], b[0])) * max(0, min(a[3], b[3])-max(a[1], b[1]))
area = lambda r: (r[2]-r[0])*(r[3]-r[1])
return inter/(area(a)+area(b)-inter)
W, H = 200, 100
box = (20,10,100,50)
x1,y1,x2,y2 = box
label = ((x1+x2)/(2*W), (y1+y2)/(2*H), (x2-x1)/W, (y2-y1)/H)
print('yolo_label', 0, *label)
truth = [(0,0,2,2)]
predictions = [(0.9, (0,0,2,2)), (0.8, (0,0,1,2))]
used, tp, fp = set(), 0, 0
for score, p in sorted(predictions, reverse=True):
candidates = [(iou(p,t),j) for j,t in enumerate(truth) if j not in used]
match = max(candidates, default=(0,-1))
if match[0] >= .5:
used.add(match[1]); tp += 1
else:
fp += 1
fn = len(truth)-len(used)
assert (tp,fp,fn) == (1,1,0)
print('IoU_half', iou((0,0,2,2),(0,0,1,2)))
print('one_to_one_TP_FP_FN', tp,fp,fn)
try:
iou((2,0,1,2), (0,0,2,2))
except ValueError:
print('invalid_box', 'rejected')
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 경로의 독자 합성 자료. Pillow 필요; 제작 시 미실행.
아무 이미지도 다운로드하지 않는다. 새 vision_data 폴더만 생성한다.
"""
import json
import random
from pathlib import Path
from PIL import Image, ImageDraw
root = Path('vision_data')
if root.exists():
raise SystemExit('vision_data가 이미 있습니다. 기존 증거를 보존하고 새 학습 폴더에서 실행하세요.')
root.mkdir()
manifest = []
for split, seed, count in [('train', 11, 40), ('val', 22, 12), ('test', 33, 12)]:
rng = random.Random(seed)
for i in range(count):
# 같은 원본에서 만든 증강판을 다른 split에 넣지 않는다.
# seed 분리는 독립 난수 실습일 뿐 실제 촬영일/장소 분리의 대체가 아니다.
for label, color in [('empty', (40, 190, 70)), ('occupied', (210, 60, 40))]:
im = Image.new('RGB', (64, 64), tuple(rng.randint(15, 35) for _ in range(3)))
d = ImageDraw.Draw(im)
x, y = rng.randint(6, 20), rng.randint(6, 20)
d.rectangle((x, y, x + 30, y + 30), fill=color)
dest = root / 'classify' / split / label / f'{split}-{i:03}.png'
dest.parent.mkdir(parents=True, exist_ok=True)
im.save(dest)
# 탐지 target=표식; 자전거 사전학습 성능으로 오인하지 않는다.
im = Image.new('RGB', (64, 64), (22, 22, 22))
mask = Image.new('L', (64, 64), 0)
d, md = ImageDraw.Draw(im), ImageDraw.Draw(mask)
x1, y1 = rng.randint(4, 26), rng.randint(4, 26)
width, height = rng.randint(16, 26), rng.randint(16, 26)
x2, y2 = x1 + width, y1 + height
# PIL rectangle의 끝점은 포함됨: 연속좌표 상자 x2/y2와 일치시키기 위해 -1.
d.rectangle((x1, y1, x2 - 1, y2 - 1), fill=(40, 190, 70))
md.rectangle((x1, y1, x2 - 1, y2 - 1), fill=1)
for kind, obj in [('images', im), ('masks', mask)]:
path = root / 'segment' / split / kind / f'{split}-{i:03}.png'
path.parent.mkdir(parents=True, exist_ok=True)
obj.save(path)
imgp = root / 'detect' / 'images' / split / f'{split}-{i:03}.png'
labp = root / 'detect' / 'labels' / split / f'{split}-{i:03}.txt'
imgp.parent.mkdir(parents=True, exist_ok=True)
labp.parent.mkdir(parents=True, exist_ok=True)
im.save(imgp)
vals = ((x1 + x2) / 128, (y1 + y2) / 128, width / 64, height / 64)
labp.write_text('0 ' + ' '.join(f'{v:.6f}' for v in vals) + '\n', encoding='utf-8')
manifest.append({'split': split, 'group': f'{split}-{i:03}', 'box_xyxy': [x1,y1,x2,y2]})
(root / 'detect' / 'data.yaml').write_text(
'path: ' + json.dumps(str((root / 'detect').resolve())) + '\n'
'train: images/train\nval: images/val\ntest: images/test\nnames:\n 0: marker\n', encoding='utf-8')
(root / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
print('created synthetic vision_data; counts per detect split: train=40 val=12 test=12')
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""Ultralytics/OpenCV 선택 실행용. 설치/모델학습/영상/카메라 제작 시 미실행.
python optional_yolo.py train
python optional_yolo.py make-video
python optional_yolo.py video --weights runs-vision/marker/weights/best.pt --source synthetic.avi
카메라는 --source camera:0 --allow-camera 를 모두 주어야 열림.
"""
import argparse
import json
import math
import time
from pathlib import Path
def train():
import ultralytics
from ultralytics import YOLO
data = Path('vision_data/detect/data.yaml')
assert data.is_file(), '먼저 prepare_assets.py 실행'
for split in ('train', 'val', 'test'):
images = sorted((data.parent / 'images' / split).glob('*.png'))
assert images, split
for image in images:
label = data.parent / 'labels' / split / (image.stem + '.txt')
for line in label.read_text().splitlines():
fields = [float(v) for v in line.split()]
assert len(fields) == 5 and fields[0] == 0, str(label)
_, x, y, w, h = fields
assert 0 < w <= 1 and 0 < h <= 1
assert 0 <= x - w / 2 < x + w / 2 <= 1
assert 0 <= y - h / 2 < y + h / 2 <= 1
# YAML은 구조만, 무작위 초기화. .pt 사전학습 가중치 자동다운로드 경로를 쓰지 않음.
model = YOLO('yolo11n.yaml')
model.train(data=str(data.resolve()), epochs=3, imgsz=64, batch=4, workers=0,
device='cpu', pretrained=False, amp=False, seed=73, deterministic=True,
project='runs-vision', name='marker', exist_ok=False, plots=False,
mosaic=0, mixup=0, degrees=0, translate=0, scale=0, fliplr=0)
best = Path(model.trainer.best)
assert best.is_file()
trained = YOLO(str(best))
# 검증에서 설정을 선택한 뒤 동결. 이 짧은 예에서는 설정을 고정하고 test는 한 번 평가.
metrics = trained.val(data=str(data.resolve()), split='test', imgsz=64, batch=4,
device='cpu', workers=0, conf=0.001, iou=0.7, plots=False)
report = {'ultralytics': ultralytics.__version__, 'weights': str(best),
'split': 'test', 'map50': float(metrics.box.map50),
'map50_95': float(metrics.box.map), 'candidate_conf': .001,
'nms_iou': .7, 'scope': 'synthetic marker; 3 epochs smoke test only'}
Path('yolo-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def make_video():
import cv2
import numpy as np
writer = cv2.VideoWriter('synthetic.avi', cv2.VideoWriter_fourcc(*'MJPG'), 10, (64, 64))
if not writer.isOpened():
raise RuntimeError('MJPG codec unavailable: use a permitted local video instead')
try:
for i in range(20):
frame = np.full((64, 64, 3), 22, dtype=np.uint8)
x = 4 + i
cv2.rectangle(frame, (x, 20), (x + 20, 40), (70, 190, 40), -1)
writer.write(frame)
finally:
writer.release()
print('synthetic.avi: intended 20 frames at 10 fps; reopen to verify')
def video(weights, source, allow_camera):
import cv2
from ultralytics import YOLO
path = Path(weights)
if not path.is_file():
raise FileNotFoundError('학습이 출력한 실제 best.pt 경로를 --weights에 입력')
if source.startswith('camera:'):
if not allow_camera:
raise PermissionError('카메라 권한/무인 촬영 동의 확인 후 --allow-camera 필요')
src = int(source.split(':')[1])
else:
if not Path(source).is_file():
raise FileNotFoundError(source)
src = source
model = YOLO(str(path))
cap = cv2.VideoCapture(src)
if not cap.isOpened():
cap.release()
raise RuntimeError('입력 열기 실패: 파일/codec/카메라 사용중 여부 확인')
durations, rows = [], []
stop_reason = 'frame_limit'
try:
# 100프레임 제한: 카메라에서도 무한 촬영/저장하지 않는다.
for index in range(100):
begin = time.perf_counter()
ok, frame = cap.read()
if not ok:
stop_reason = 'read_false_eof_or_failure'
break
result = model.predict(frame, device='cpu', imgsz=64, conf=.25, iou=.7, verbose=False)[0]
item = {'frame': index, 'boxes': result.boxes.xyxy.tolist(),
'classes': result.boxes.cls.int().tolist(), 'scores': result.boxes.conf.tolist()}
# 원본 프레임은 저장하지 않고 후보 좌표만 저장. 이 좌표도 운영에서는 보존기한 필요.
rows.append(item)
durations.append((time.perf_counter() - begin) * 1000)
finally:
cap.release()
if not rows:
raise RuntimeError('0 frames: 성공 처리 금지. 입력 codec/카메라를 복구하세요.')
# 전체 지연은 read+CPU추론+결과 구조화. 디스크쓰기/네트워크/GUI 지연은 제외.
ordered = sorted(durations)
report = {'frames': len(rows), 'frame_detection_sum': sum(len(r['boxes']) for r in rows),
'unique_objects': None, 'mean_ms': sum(durations) / len(durations),
'p95_ms_nearest_rank': ordered[math.ceil(.95 * len(ordered)) - 1],
'cold_start_included': True, 'stop_reason': stop_reason, 'frame_limit': 100,
'scope': 'read+predict+structure; not disk/network/UI; read=False may be EOF or decode/device failure'}
Path('video-detections.json').write_text(json.dumps(rows, indent=2), encoding='utf-8')
Path('video-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['train', 'make-video', 'video'])
p.add_argument('--weights', default='runs-vision/marker/weights/best.pt')
p.add_argument('--source', default='synthetic.avi')
p.add_argument('--allow-camera', action='store_true')
a = p.parse_args()
if a.mode == 'train':
train()
elif a.mode == 'make-video':
make_video()
else:
video(a.weights, a.source, a.allow_camera)
if __name__ == '__main__':
main()
YOLO 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
라벨은 0 .7 .5 .4 .6. 두 상자의 교집합2/합집합6이므로 IoU1/3으로 기준 미달. 예측은 FP1, 정답은 FN1, TP0. 점수 .99라도 위치가 기준을 못 넘으면 TP가 아니다.
평가 기준: 자동: 정규화 네 값과 IoU·TP0/FP1/FN1. 자기평가: 점수/NMS/평가 IoU 세 기준의 역할. 선택 실학습 여부는 별도 증거 없으면 미실행.
새 맥락에 적용하기
손으로 그린 조립 부품 위치 찾기로 옮겨 한 부품의 부분 가림·프레임 절단·서로 붙음 라벨 정책을 세 문장으로 정하고 두 라벨러가 같은 상자를 그리는지 비교하라.
내 말로 설명하고 가르치기
학생은 정답 토큰 하나를 실제로 사용중 상자로 옮겨 중복 채점을 막는 과정을 보인다. 강사는 클래스가 다를 때도 TP로 세어도 되는지 묻는다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
https://docs.ultralytics.com/models/yolo11/ — YOLO11 Overview, backbone/neck 특징, Supported Tasks and Modes의 파일명, Usage Examples의 YAML/.pt 입력, AGPL-3.0/Enterprise 안내 본문을 읽음. 벤치마크를 자체 성능으로 주장하지 않음. 라이선스 법률 검토 아님.
https://docs.ultralytics.com/modes/val/ — 반환된 Val 사용 예와 Arguments 표의 split, conf, iou(NMS), workers, metrics.box.map/map50 설명을 읽음. 현행 기본 설명이 YOLO26인 점을 확인; 본 예제는 YOLO11 고정 설계이며 실행 호환성은 미검증.
같은 자전거가 세 프레임에 보이자 세 대가 지나갔다고 보고했다. 평균 지연만 보고 실시간이라는 이름도 붙였다.
프레임 처리·추적 ID·고유 개수의 차이와 평균/p95 지연을 계산하고 영상 파일 처리/실패복구를 완성한다.
먼저 알아둘 것
vision-detect: 후보 상자와 점수
핵심 한 문장: 프레임마다 세는 합은 관측 횟수이지 고유 물체 수가 아니다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
한 사람이 사진 세 장에 찍혔다고 사람이 세 명으로 늘지 않는다.
비유가 닿지 않는 곳: 추적 ID도 신분증이 아니다. 가림·화면 이탈·재등장으로 같은 물체에 새 ID가 붙을 수 있다.
실제로 작동하는 원리
영상은 시간순 프레임과 시각 정보다. 검출은 각 프레임 안의 후보, 추적은 프레임 사이 후보를 같은 대상이라고 연결하는 추가 알고리즘이다. 고유 통과 수가 필요하면 ID뿐 아니라 기준선 통과 방향·재진입 정책·ID switch 평가까지 있어야 한다. 본 선택 코드는 검출만 구현하므로 unique_objects를 null로 기록한다.
기본 모의 ID7이 세 프레임에 반복되면 상자 관측합3, 관측된 ID수1이다. ID수1도 실제 유일한 자전거가 한 대라는 증명은 아니다. 지연 모의값40/60/200ms의 평균은100ms, nearest-rank p95는200ms다. 20fps를 모두 처리하려면 프레임당 예산50ms가 필요하므로 이 입력은 그 예산을 만족하지 못한다. 작은 세 표본의 p95를 운영 통계처럼 일반화하지 않는다.
선택 경로는 OpenCV read→YOLO CPU predict→좌표 구조화까지만 측정한다. 첫 추론의 초기화 지연을 포함하고, 디스크 쓰기/네트워크/GUI 표시는 제외한다. 입력 영상 fps와 실제 처리 throughput을 혼동하지 않는다. 온라인 카메라에서 처리 속도가 느리면 오래된 프레임이 밀릴 수 있어 최신 프레임 선택·드롭 수·촬영 시각과 표시 시각 차이를 별도로 설계해야 한다. 본 교재는 실시간 SLA를 보장하지 않는다.
선택 보고서는 frame_limit=100과 stop_reason을 저장한다. frame_limit은 전체 영상 처리가 끝났다는 뜻이 아니다. read_false_eof_or_failure는 정상 파일 끝/디코딩 실패/장치중단을 read=False만으로 구별할 수 없다는 표시다. 기대 프레임수·입력 길이·외부 장치 상태와 추가 대조하고 부분 처리로 보고한다.
따라 하기
python3 vision-video.py로 모의 ID 합계와 지연 계산을 실행한다.
선택: YOLO 학습 후 python optional_yolo.py make-video로 합성 표식이 이동하는 AVI를 만든다. 코덱이 없으면 실패를 기록하고 허용된 로컬 영상 파일을 사용한다.
python optional_yolo.py video --weights 실제_best.pt_경로 --source synthetic.avi를 실행한다. 실제_best.pt_경로는 앞 학습의 yolo-report.json weights 값으로 대체한다. 영상파일은 로컬 경로만 받는다.
video-report.json과 프레임별 좌표를 대조한다. 카메라는 기본 비활성이고 무인 장면 허가 후 --source camera:0 --allow-camera를 모두 지정해야 한다. 제작 시 카메라·영상·모델 실행은 하지 않았다.
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
frame_detection_sum 3
observed_track_ids 1
mean_ms 100.0 p95_ms 200
20fps_budget_ms 50.0 within_budget False
검증 방법
frame_detection_sum=3과 observed_track_ids=1을 구분해 읽는다. p95 정의가 nearest-rank인지 기록한다.
선택 합성 AVI는 생성 의도20프레임을 실제 reopen한 frames 값과 비교해야 한다. 0프레임은 성공이 아니라 예외다. 검출 개수0은 모델이 아무것도 찾지 못한 정상 결과일 수 있다.
release가 예외에서도 실행되는 finally를 확인한다. 장치가 계속 잠기면 프로세스 종료 후 소유 프로그램을 확인하고 무단 강제 종료는 하지 않는다.
흔한 오해와 실패 복구
파일 끝의 read=False와 첫 프레임부터 읽기 실패를 구분한다. 처음부터0프레임이면 경로/codec/권한을 복구한다.
FPS 목표를 맞추려고 프레임을 버리면 버린 개수와 영향도 함께 기록한다. 모든 프레임을 처리한 것처럼 발표하지 않는다.
추적 구현 없이 ID를 좌표 해시로 붙이는 편법은 재등장/가림 고유개수 문제를 해결하지 못한다.
확인 문제
동일 추적 ID7이 세 프레임에 보였다. 확실히 계산할 수 있는 것은?
자동채점 · 선택형
해설과 정답 보기
관측 횟수와 ID 종류수만 제공 데이터에서 계산된다.
정답: 관측3회와 관측ID1개
선택 코드의 mean_ms에 디스크저장과 화면표시가 빠졌다면 어떤 표현을 피해야 하는가?
자기평가 · 자동채점 아님
평가 기준: 측정 구간과 제외된 디스크/GUI를 명시하고 end-to-end라고 주장하지 않으면 통과.
해설 보기
종단간 사용자 지연 또는 전체 서비스 실시간 성능이라고 부르면 안 된다. read+predict+구조화 구간의 측정이라고 한정한다.
독립 실습
새 모의 ID 목록을 [7],[7,9],[9]로, 지연을 [30,40,80,90]ms로 바꾸어 관측합/ID수/평균/p95를 계산하라. 표본수와 프레임 ID 예제가 별도 실험이라는 점도 표시한다.
남길 증거: 새 모의 입력·stdout·지연 구간 명세. 선택 시 실제 영상 frames와 평균/p95, codec 실패 여부, 카메라 사용 여부.
기본 worked example — 표준라이브러리 실제 검산
from math import ceil
# 직접 만든 가상 추적 결과. 추적기/카메라 실행 아님.
frames = [{'frame': 0, 'ids': [7]}, {'frame': 1, 'ids': [7]}, {'frame': 2, 'ids': [7]}]
latency_ms = [40, 60, 200]
print('frame_detection_sum', sum(len(f['ids']) for f in frames))
print('observed_track_ids', len({i for f in frames for i in f['ids']}))
print('mean_ms', round(sum(latency_ms)/len(latency_ms), 1), 'p95_ms', sorted(latency_ms)[ceil(.95*len(latency_ms))-1])
print('20fps_budget_ms', 1000/20, 'within_budget', all(x<=1000/20 for x in latency_ms))
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""Ultralytics/OpenCV 선택 실행용. 설치/모델학습/영상/카메라 제작 시 미실행.
python optional_yolo.py train
python optional_yolo.py make-video
python optional_yolo.py video --weights runs-vision/marker/weights/best.pt --source synthetic.avi
카메라는 --source camera:0 --allow-camera 를 모두 주어야 열림.
"""
import argparse
import json
import math
import time
from pathlib import Path
def train():
import ultralytics
from ultralytics import YOLO
data = Path('vision_data/detect/data.yaml')
assert data.is_file(), '먼저 prepare_assets.py 실행'
for split in ('train', 'val', 'test'):
images = sorted((data.parent / 'images' / split).glob('*.png'))
assert images, split
for image in images:
label = data.parent / 'labels' / split / (image.stem + '.txt')
for line in label.read_text().splitlines():
fields = [float(v) for v in line.split()]
assert len(fields) == 5 and fields[0] == 0, str(label)
_, x, y, w, h = fields
assert 0 < w <= 1 and 0 < h <= 1
assert 0 <= x - w / 2 < x + w / 2 <= 1
assert 0 <= y - h / 2 < y + h / 2 <= 1
# YAML은 구조만, 무작위 초기화. .pt 사전학습 가중치 자동다운로드 경로를 쓰지 않음.
model = YOLO('yolo11n.yaml')
model.train(data=str(data.resolve()), epochs=3, imgsz=64, batch=4, workers=0,
device='cpu', pretrained=False, amp=False, seed=73, deterministic=True,
project='runs-vision', name='marker', exist_ok=False, plots=False,
mosaic=0, mixup=0, degrees=0, translate=0, scale=0, fliplr=0)
best = Path(model.trainer.best)
assert best.is_file()
trained = YOLO(str(best))
# 검증에서 설정을 선택한 뒤 동결. 이 짧은 예에서는 설정을 고정하고 test는 한 번 평가.
metrics = trained.val(data=str(data.resolve()), split='test', imgsz=64, batch=4,
device='cpu', workers=0, conf=0.001, iou=0.7, plots=False)
report = {'ultralytics': ultralytics.__version__, 'weights': str(best),
'split': 'test', 'map50': float(metrics.box.map50),
'map50_95': float(metrics.box.map), 'candidate_conf': .001,
'nms_iou': .7, 'scope': 'synthetic marker; 3 epochs smoke test only'}
Path('yolo-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def make_video():
import cv2
import numpy as np
writer = cv2.VideoWriter('synthetic.avi', cv2.VideoWriter_fourcc(*'MJPG'), 10, (64, 64))
if not writer.isOpened():
raise RuntimeError('MJPG codec unavailable: use a permitted local video instead')
try:
for i in range(20):
frame = np.full((64, 64, 3), 22, dtype=np.uint8)
x = 4 + i
cv2.rectangle(frame, (x, 20), (x + 20, 40), (70, 190, 40), -1)
writer.write(frame)
finally:
writer.release()
print('synthetic.avi: intended 20 frames at 10 fps; reopen to verify')
def video(weights, source, allow_camera):
import cv2
from ultralytics import YOLO
path = Path(weights)
if not path.is_file():
raise FileNotFoundError('학습이 출력한 실제 best.pt 경로를 --weights에 입력')
if source.startswith('camera:'):
if not allow_camera:
raise PermissionError('카메라 권한/무인 촬영 동의 확인 후 --allow-camera 필요')
src = int(source.split(':')[1])
else:
if not Path(source).is_file():
raise FileNotFoundError(source)
src = source
model = YOLO(str(path))
cap = cv2.VideoCapture(src)
if not cap.isOpened():
cap.release()
raise RuntimeError('입력 열기 실패: 파일/codec/카메라 사용중 여부 확인')
durations, rows = [], []
stop_reason = 'frame_limit'
try:
# 100프레임 제한: 카메라에서도 무한 촬영/저장하지 않는다.
for index in range(100):
begin = time.perf_counter()
ok, frame = cap.read()
if not ok:
stop_reason = 'read_false_eof_or_failure'
break
result = model.predict(frame, device='cpu', imgsz=64, conf=.25, iou=.7, verbose=False)[0]
item = {'frame': index, 'boxes': result.boxes.xyxy.tolist(),
'classes': result.boxes.cls.int().tolist(), 'scores': result.boxes.conf.tolist()}
# 원본 프레임은 저장하지 않고 후보 좌표만 저장. 이 좌표도 운영에서는 보존기한 필요.
rows.append(item)
durations.append((time.perf_counter() - begin) * 1000)
finally:
cap.release()
if not rows:
raise RuntimeError('0 frames: 성공 처리 금지. 입력 codec/카메라를 복구하세요.')
# 전체 지연은 read+CPU추론+결과 구조화. 디스크쓰기/네트워크/GUI 지연은 제외.
ordered = sorted(durations)
report = {'frames': len(rows), 'frame_detection_sum': sum(len(r['boxes']) for r in rows),
'unique_objects': None, 'mean_ms': sum(durations) / len(durations),
'p95_ms_nearest_rank': ordered[math.ceil(.95 * len(ordered)) - 1],
'cold_start_included': True, 'stop_reason': stop_reason, 'frame_limit': 100,
'scope': 'read+predict+structure; not disk/network/UI; read=False may be EOF or decode/device failure'}
Path('video-detections.json').write_text(json.dumps(rows, indent=2), encoding='utf-8')
Path('video-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['train', 'make-video', 'video'])
p.add_argument('--weights', default='runs-vision/marker/weights/best.pt')
p.add_argument('--source', default='synthetic.avi')
p.add_argument('--allow-camera', action='store_true')
a = p.parse_args()
if a.mode == 'train':
train()
elif a.mode == 'make-video':
make_video()
else:
video(a.weights, a.source, a.allow_camera)
if __name__ == '__main__':
main()
영상 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
관측합4, ID수2. 별도 지연표 평균60ms, nearest-rank p95=90ms. 20fps 예산50ms는 두 프레임이 넘는다. ID 예제3프레임과 지연표4건을 같은 프레임의 측정처럼 합치지 않는다.
평가 기준: 자동: 네 계산값과 예산초과2건. 자기평가: 관측 ID수/실물수 및 두 실험 입력을 분리하여 보고. 카메라 미실행을 숨기면 불통과.
새 맥락에 적용하기
합성 컨베이어 표식 영상에서 통과선을 한 방향으로 지난 횟수를 세려면 필요한 track 이벤트 계약을 설계하라. 재진입/가림 실패 사례를 포함하되 실제 장비 제어는 하지 않는다.
내 말로 설명하고 가르치기
학생은 같은 물체 종이를 세 번 보여주고 관측과 고유 개수를 설명한다. 강사는 ID가7→12로 바뀌는 경우를 제시하고 왜 재검토가 필요한지 묻는다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
https://docs.ultralytics.com/models/yolo11/ — YOLO11 Overview, backbone/neck 특징, Supported Tasks and Modes의 파일명, Usage Examples의 YAML/.pt 입력, AGPL-3.0/Enterprise 안내 본문을 읽음. 벤치마크를 자체 성능으로 주장하지 않음. 라이선스 법률 검토 아님.
학습 단원
상자 대신 칸을 칠하고 개체 이름 붙이기
해결할 문제와 목표
보관대 표식의 벗겨진 부분만 표시하고 싶은데 상자는 배경을 너무 많이 포함한다. 같은 색 두 표식을 하나의 개체로 오인하기도 한다.
의미론적/인스턴스 마스크 차이와 픽셀IoU를 계산하고, 선택 PyTorch 픽셀 분류 학습과 OpenCV 연결요소의 한계를 검증한다.
먼저 알아둘 것
vision-learn: CNN과 손실
vision-detect: IoU
핵심 한 문장: 마스크는 어떤 칸이 속하는지, 인스턴스 ID는 어느 물체의 칸인지 말한다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
같은 색 색칠공부에도 서로 다른 두 풍선에는 각자 이름표를 붙일 수 있다.
비유가 닿지 않는 곳: 같은 색 영역이 붙어 있으면 단순 연결요소는 둘을 구분하지 못한다. 색칠은 손상의 원인 진단이 아니다.
실제로 작동하는 원리
의미분할의 class1 픽셀이 두 덩어리여도 같은 클래스다. 인스턴스분할은 덩어리별 id1/id2를 붙이고 각 ID의 클래스도 보존한다. 배경은0으로 둔다. 마스크는 상자보다 세밀하지만 정답 경계 자체도 라벨러마다 다를 수 있어 작은 틈·반사·가림의 기준이 필요하다.
기본 truth=110/010, pred=100/011에서 겹친 칸은 좌상단과 가운데 아래2개, 합집합은4개, IoU=.5다. 전체 배경 칸을 많이 맞히는 픽셀 정확도만 쓰면 작은 결함 누락을 숨길 수 있다. foreground IoU와 결함 개체별 재현율을 따로 본다. 두 마스크가 모두 비면 foreground IoU는 이 수식에서0/0이며 본 학습 코드 정책은 None이다. 무결함 정확도는 별도 계산해야 한다.
선택 코드 semantic_mask는 합성 이미지 RGB를 입력받아 각 픽셀 두 logit(배경/표식)을 내고 CrossEntropyLoss로 학습한다. val IoU로 best 가중치를 선택한 후 test foreground IoU와 pred/truth PNG를 저장한다. 저장 마스크는 보기에 편하도록0/255이지만 학습 정답은0/1 정수다. OpenCV connectedComponents는 예측 이진 마스크의 연결된 영역을 나눈 후 ID 배열을 저장한다. 이것을 학습된 인스턴스분할 모델이나 겹친 개체 분리 성공으로 부르지 않는다.
따라 하기
두 2×3 마스크를 그려 교집합과 합집합에 다른 표시를 한다. python3 vision-segment.py로 검산한다.
semantic 101/101과 instance 102/102가 같은 foreground를 가진다는 assert를 확인한다.
선택: setup의 PyTorch/OpenCV를 준비하고 아직 생성하지 않았다면 python prepare_assets.py, 이후 python optional_models.py mask.
test_iou와 mask-prediction.png/ mask-truth.png를 대조하고 mask-component-ids.npy의 ID 개수를 기록한다. 선택 라이브러리/학습은 제작 시 미실행이다.
입력 예제
정답 [[1,1,0],[0,1,0]], 예측 [[1,0,0],[0,1,1]]. 별도 개체 비교 semantic=[[1,0,1],[1,0,1]], instances=[[1,0,2],[1,0,2]].
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
mask_intersection_union_iou 2 4 0.5
semantic_classes [1] instance_ids [1, 2]
검증 방법
기본 IoU=.5의 교집합2/합집합4를 좌표로 설명한다. 개체 비교는 IoU 입력과 별도 도식임을 표시한다.
선택 train/val/test 마스크 값이0/1인지 확인한다. prediction/truth 크기가 같아야 비교 가능하다. 정답 마스크에는 nearest 외 보간을 적용하지 않는다.
흔한 오해와 실패 복구
마스크 해상도가 다르면 좌표 변환을 먼저 확정한다. 그냥 작은 쪽 길이에 맞춰 zip하면 일부 칸이 사라진다.
연결요소 ID가2개라고 실제 개체가 반드시2개인 것은 아니다. 한 표식이 끊어지면 둘, 두 표식이 붙으면 하나가 될 수 있다.
분할 결과로 설비를 자동 정지하거나 의료진단을 내리지 않는다. 의료영상은 식별정보·기관 규정·전문가 정답·임상 검증이 추가되어야 하므로 이 합성 실습에서 제외한다.
확인 문제
동일 클래스 두 개체를 구분할 정보가 더 필요한 것은?
자동채점 · 선택형
해설과 정답 보기
클래스 마스크 외 개체별 ID가 있어야 같은 클래스의 서로 다른 개체를 구분할 수 있다.
정답: 인스턴스 ID
두 사각형이 붙었을 때 connectedComponents가 하나를 반환할 수 있는 이유와 주장하면 안 되는 성과는?
자기평가 · 자동채점 아님
평가 기준: 연결 규칙의 한계와 learned instance segmentation과의 구분을 모두 설명하면 통과.
해설 보기
이웃 foreground의 연결 여부만 보기 때문에 두 물체가 붙으면 하나로 묶인다. 학습된 인스턴스분할로 둘을 정확히 분리했다고 말하면 안 된다.
독립 실습
독립 truth=101/000, pred=100/001의 IoU를 구하라. 그다음 모든 칸이0인 두 마스크를 넣었을 때 0으로 나누지 않도록 None 정책을 구현하라.
남길 증거: 독립 마스크 계산·빈합집합 분기·연결요소 반례. 선택 시 실제 mask PNG/모델/test IoU 보고.
기본 worked example — 표준라이브러리 실제 검산
truth = [[1,1,0],[0,1,0]]
pred = [[1,0,0],[0,1,1]]
flat_t = sum(truth, [])
flat_p = sum(pred, [])
inter = sum(a == b == 1 for a,b in zip(flat_t,flat_p))
union = sum(a == 1 or b == 1 for a,b in zip(flat_t,flat_p))
assert (inter,union) == (2,4)
print('mask_intersection_union_iou', inter, union, inter/union)
semantic = [[1,0,1],[1,0,1]]
instances = [[1,0,2],[1,0,2]]
assert [[int(v>0) for v in r] for r in instances] == semantic
print('semantic_classes', sorted(set(sum(semantic, [])) - {0}), 'instance_ids', sorted(set(sum(instances, []))-{0}))
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 경로의 독자 합성 자료. Pillow 필요; 제작 시 미실행.
아무 이미지도 다운로드하지 않는다. 새 vision_data 폴더만 생성한다.
"""
import json
import random
from pathlib import Path
from PIL import Image, ImageDraw
root = Path('vision_data')
if root.exists():
raise SystemExit('vision_data가 이미 있습니다. 기존 증거를 보존하고 새 학습 폴더에서 실행하세요.')
root.mkdir()
manifest = []
for split, seed, count in [('train', 11, 40), ('val', 22, 12), ('test', 33, 12)]:
rng = random.Random(seed)
for i in range(count):
# 같은 원본에서 만든 증강판을 다른 split에 넣지 않는다.
# seed 분리는 독립 난수 실습일 뿐 실제 촬영일/장소 분리의 대체가 아니다.
for label, color in [('empty', (40, 190, 70)), ('occupied', (210, 60, 40))]:
im = Image.new('RGB', (64, 64), tuple(rng.randint(15, 35) for _ in range(3)))
d = ImageDraw.Draw(im)
x, y = rng.randint(6, 20), rng.randint(6, 20)
d.rectangle((x, y, x + 30, y + 30), fill=color)
dest = root / 'classify' / split / label / f'{split}-{i:03}.png'
dest.parent.mkdir(parents=True, exist_ok=True)
im.save(dest)
# 탐지 target=표식; 자전거 사전학습 성능으로 오인하지 않는다.
im = Image.new('RGB', (64, 64), (22, 22, 22))
mask = Image.new('L', (64, 64), 0)
d, md = ImageDraw.Draw(im), ImageDraw.Draw(mask)
x1, y1 = rng.randint(4, 26), rng.randint(4, 26)
width, height = rng.randint(16, 26), rng.randint(16, 26)
x2, y2 = x1 + width, y1 + height
# PIL rectangle의 끝점은 포함됨: 연속좌표 상자 x2/y2와 일치시키기 위해 -1.
d.rectangle((x1, y1, x2 - 1, y2 - 1), fill=(40, 190, 70))
md.rectangle((x1, y1, x2 - 1, y2 - 1), fill=1)
for kind, obj in [('images', im), ('masks', mask)]:
path = root / 'segment' / split / kind / f'{split}-{i:03}.png'
path.parent.mkdir(parents=True, exist_ok=True)
obj.save(path)
imgp = root / 'detect' / 'images' / split / f'{split}-{i:03}.png'
labp = root / 'detect' / 'labels' / split / f'{split}-{i:03}.txt'
imgp.parent.mkdir(parents=True, exist_ok=True)
labp.parent.mkdir(parents=True, exist_ok=True)
im.save(imgp)
vals = ((x1 + x2) / 128, (y1 + y2) / 128, width / 64, height / 64)
labp.write_text('0 ' + ' '.join(f'{v:.6f}' for v in vals) + '\n', encoding='utf-8')
manifest.append({'split': split, 'group': f'{split}-{i:03}', 'box_xyxy': [x1,y1,x2,y2]})
(root / 'detect' / 'data.yaml').write_text(
'path: ' + json.dumps(str((root / 'detect').resolve())) + '\n'
'train: images/train\nval: images/val\ntest: images/test\nnames:\n 0: marker\n', encoding='utf-8')
(root / 'manifest.json').write_text(json.dumps(manifest, indent=2), encoding='utf-8')
print('created synthetic vision_data; counts per detect split: train=40 val=12 test=12')
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 실행 전용. 이번 원고 제작에서는 import/학습/다운로드하지 않음.
Python 3.11+의 별도 가상환경에서 실행. 모든 출력은 현재 학습 폴더에만 저장.
"""
import argparse
import copy
import json
from pathlib import Path
def classification(transfer=False, allow_download=False):
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, models, transforms
torch.manual_seed(73)
torch.set_num_threads(2)
weights = models.ResNet18_Weights.IMAGENET1K_V1 if transfer else None
if transfer and not allow_download:
raise SystemExit('전이학습 가중치 다운로드/라이선스 검토 후 --allow-download 필요')
transform = weights.transforms() if transfer else transforms.Compose([
transforms.Resize((64, 64)), transforms.ToTensor()])
data = {s: datasets.ImageFolder(Path('vision_data/classify') / s, transform)
for s in ('train', 'val', 'test')}
assert all(d.class_to_idx == data['train'].class_to_idx for d in data.values())
assert all(len(d) > 0 for d in data.values())
nclass = len(data['train'].classes)
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train'), num_workers=0)
for s, d in data.items()}
if transfer:
model = models.resnet18(weights=weights)
for p in model.parameters():
p.requires_grad_(False)
model.fc = nn.Linear(model.fc.in_features, nclass)
else:
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), nn.Conv2d(8, 16, 3, padding=1),
nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(16, nclass))
optimizer = torch.optim.Adam((p for p in model.parameters() if p.requires_grad), lr=0.003)
loss_fn = nn.CrossEntropyLoss()
frozen_before = {k: v.clone() for k, v in model.state_dict().items()
if transfer and not k.startswith('fc.')}
best_score, best_state = -1, None
log = []
for epoch in range(5):
# 동결 backbone의 BatchNorm running statistics까지 유지한다.
if transfer:
model.eval()
model.fc.train()
else:
model.train()
total_loss = 0.0
for x, y in loaders['train']:
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
model.eval()
with torch.inference_mode():
correct = sum((model(x).argmax(1) == y).sum().item() for x, y in loaders['val'])
val_accuracy = correct / len(data['val'])
log.append({'epoch': epoch + 1, 'train_loss': total_loss / len(data['train']),
'val_accuracy': val_accuracy})
if val_accuracy > best_score:
best_score, best_state = val_accuracy, copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
model.eval()
matrix = [[0] * nclass for _ in range(nclass)]
with torch.inference_mode():
for x, y in loaders['test']:
for truth, pred in zip(y.tolist(), model(x).argmax(1).tolist()):
matrix[truth][pred] += 1
if transfer:
assert all(torch.equal(v, model.state_dict()[k]) for k, v in frozen_before.items())
prefix = 'transfer' if transfer else 'cnn'
torch.save({'state_dict': best_state, 'classes': data['train'].classes}, prefix + '.pt')
report = {'mode': prefix, 'classes': data['train'].classes, 'epochs': log,
'test_confusion_rows_truth': matrix, 'test_n': len(data['test']),
'scope': 'synthetic colored plates only; NOT bicycle deployment'}
Path(prefix + '-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def semantic_mask():
import numpy as np
from PIL import Image
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
torch.manual_seed(73)
torch.set_num_threads(2)
class Pairs(Dataset):
def __init__(self, split):
self.files = sorted((Path('vision_data/segment') / split / 'images').glob('*.png'))
assert self.files, '먼저 prepare_assets.py 실행'
def __len__(self):
return len(self.files)
def __getitem__(self, i):
p = self.files[i]
x = np.array(Image.open(p).convert('RGB'), dtype=np.float32) / 255
m = np.array(Image.open(p.parent.parent / 'masks' / p.name), dtype=np.int64)
assert m.shape == x.shape[:2] and set(np.unique(m)).issubset({0, 1}), 'mask shape/labels invalid'
return torch.from_numpy(x).permute(2, 0, 1), torch.from_numpy(m)
sets = {s: Pairs(s) for s in ('train', 'val', 'test')}
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train')) for s, d in sets.items()}
# 배경0/합성 표식1: 각 픽셀의 두 logit을 학습한다. 사전학습 아님.
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.Conv2d(8, 2, 1))
opt = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
def score(split):
inter = union = 0
model.eval()
with torch.inference_mode():
for x, y in loaders[split]:
pred = model(x).argmax(1)
inter += ((pred == 1) & (y == 1)).sum().item()
union += ((pred == 1) | (y == 1)).sum().item()
return inter / union if union else None
best, state = -1, None
for epoch in range(8):
model.train()
for x, y in loaders['train']:
opt.zero_grad()
loss = criterion(model(x), y)
loss.backward()
opt.step()
val_iou = score('val')
print('epoch', epoch + 1, 'val_iou', val_iou)
if val_iou is not None and val_iou > best:
best, state = val_iou, copy.deepcopy(model.state_dict())
assert state is not None
model.load_state_dict(state)
test_iou = score('test')
x, truth = sets['test'][0]
with torch.inference_mode():
pred = model(x.unsqueeze(0)).argmax(1)[0].numpy().astype('uint8')
Image.fromarray(pred * 255).save('mask-prediction.png')
Image.fromarray(truth.numpy().astype('uint8') * 255).save('mask-truth.png')
torch.save(state, 'mask-model.pt')
print(json.dumps({'test_foreground_iou': test_iou, 'test_n': len(sets['test'])}))
# 연결요소는 붙은 물체를 분리하지 못한다. 학습된 인스턴스 분할 모델이 아니다.
import cv2
n, instance_ids = cv2.connectedComponents(pred, connectivity=4)
np.save('mask-component-ids.npy', instance_ids)
print('connected_components_not_true_instances', n - 1)
def anomaly():
import numpy as np
from PIL import Image
from sklearn.ensemble import IsolationForest
rng = np.random.default_rng(73)
def image(defect=False, dark=False):
a = rng.normal(120, 3, (32, 32))
if defect:
a[12:18, 12:18] = 220
if dark:
a -= 40
return np.clip(a, 0, 255).astype(np.uint8)
def feature(a):
return [float(a.mean()), float(a.std()), float(np.quantile(a, .99))]
# 정상만 fit; 독립 정상 val로 threshold; test를 threshold 설정에 사용하지 않는다.
train = [image() for _ in range(80)]
val = [image() for _ in range(30)]
model = IsolationForest(n_estimators=80, contamination='auto', random_state=73, n_jobs=1)
model.fit([feature(a) for a in train])
threshold = float(np.quantile(-model.score_samples([feature(a) for a in val]), .95))
tests = [('normal', image()), ('bright-patch', image(defect=True)), ('dark-normal', image(dark=True))]
report = []
for name, a in tests:
score = float(-model.score_samples([feature(a)])[0])
report.append({'case': name, 'score': score, 'threshold': threshold,
'review_candidate': score > threshold})
Image.fromarray(a).save('anomaly-' + name + '.png')
Path('anomaly-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
# 통계적 변화 후보일 뿐 결함 종류/의료 진단/설비 정지 명령이 아니다.
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['cnn', 'transfer', 'mask', 'anomaly'])
p.add_argument('--allow-download', action='store_true')
a = p.parse_args()
if a.mode == 'cnn':
classification()
elif a.mode == 'transfer':
classification(True, a.allow_download)
elif a.mode == 'mask':
semantic_mask()
else:
anomaly()
if __name__ == '__main__':
main()
마스크 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
교집합1, 합집합3, IoU1/3. 빈 마스크 쌍은 union=0이므로 None을 내고 무결함 케이스 수를 별도 보고한다. 이를 IoU100%로 조용히 평균에 넣지 않는다.
평가 기준: 자동: 1/3과 빈 입력 None. 자기평가: 클래스와 ID 차이, 겹친 개체의 연결요소 실패. 실제 test IoU는 선택 실행 전에는 미산출로 남긴다.
새 맥락에 적용하기
직접 그린 도자기 무늬의 벗겨짐 후보 마스크로 전이하여 경계 라벨 정책과 전문가 검토 범위를 적어라. 작품 원인/가격 판단으로 확장하지 않는다.
내 말로 설명하고 가르치기
학생이 같은 클래스 두 영역에 다른 ID를 붙인다. 강사는 두 영역 사이에 한 칸 다리를 추가하고 연결요소와 실제 개체수가 달라지는 이유를 묻는다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
표식이 어두워졌다는 이유만으로 손상이라고 표시했다. 실제로는 조명이 꺼졌고 표식은 멀쩡했다.
정상 학습·검증 임계값·독립 시험을 분리하고 이상점수와 결함/질병 판정을 구분하는 파이프라인을 만든다.
먼저 알아둘 것
vision-segment: 영역과 라벨
vision-evaluate: FP/FN
핵심 한 문장: 이상 탐지는 정상과 다름을 찾으며, 다르다는 이유만으로 고장은 확정되지 않는다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
평소와 다른 소리가 나면 점검 목록에 올리는 관리인이지, 소리만 듣고 부품 교체를 확정하는 판사가 아니다.
비유가 닿지 않는 곳: 정상 분포 자체가 바뀌면 건강한 물체도 낯설게 보인다. 드물지만 이미 학습된 결함은 오히려 정상으로 처리될 수 있다.
실제로 작동하는 원리
정상 자료만으로 기준을 만든 뒤 각 새 입력의 차이를 점수로 표현하는 방법이 있다. 교실 모형은 정상 밝기[98,100,102]의 평균100을 중심으로 하고 절대거리 |x-100|를 이상점수로 쓴다. 독립 정상 val=[99,101,103]의 최대거리3을 임계값으로 고정한다. 시험101은 점수1로 통과,112는12로 검토 후보, 조명만 어두운94는6으로 역시 후보다. 마지막은 결함이 없는 FP 반례다.
선택 IsolationForest는 이미지 평균·표준편차·상위 밝기 분위수라는 세 특징을 사용한다. 임의 특징과 분할값으로 샘플을 분리할 때 빨리 고립되는 관측을 낯설다고 보는 모델이다. 정상 합성 이미지80장에 fit, 별도 정상30장의 음수 score_samples 값95분위수를 threshold로 정하고 시험의 점수가 더 크면 검토 후보로 둔다. 점수는 확률이 아니며 서로 다른 모델 설정의 숫자를 직접 비교하지 않는다.
이 단순 특징은 작은 긁힘을 평균에 묻어버리거나 조명 변화에 과민할 수 있다. 픽셀 위치가 필요한 검토는 앞 단원의 마스크 모델 또는 잔차지도와 연결해야 한다. 이상점수만으로 손상 위치를 꾸며내지 않는다. 파이프라인은 입력 품질 검사→정상과 비교→후보 표시→원본/마스크/관측조건 대조→사람 검토다. 산업 현장에서는 오탐으로 인한 불필요한 중단과 누락 비용을 함께 평가하고, 의료 분야에서는 임상 라벨·민감정보·전문가 책임 때문에 이 교육 데모를 그대로 전용할 수 없다.
따라 하기
python3 vision-anomaly.py를 실행하여 dim_but_intact가 왜 후보인지 추적한다.
기본 점수의 중심은 train만, threshold는 val만으로 계산했는지 코드에서 색으로 표시한다.
선택: setup의 scikit-learn/numpy/Pillow를 설치하고 python optional_models.py anomaly. prepare_assets.py는 이 경로에서는 필요 없고 코드가 메모리에서 합성 정상/시험 영상을 만든다.
anomaly-report.json과 세 PNG를 비교하여 점수·임계값·review_candidate를 기록한다. 실제 라이브러리 결과는 미리 지정하지 않으며 제작 시 미실행이다.
입력 예제
정상train=[98,100,102], 정상val=[99,101,103], 시험 normal101 / scratch_candidate112 / dim_but_intact94. 결함 라벨은 시나리오 설명이며 밝기만으로 증명되지 않는다.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
normal_center 100 validation_threshold 3
normal score 1 review False
scratch_candidate score 12 review True
dim_but_intact score 6 review True
검증 방법
중심100·threshold3과 세 점수1/12/6을 검산한다. dark-normal 후보는 명시적 실패 사례로 남긴다.
선택 보고서에 모든 케이스의 score/threshold가 있고 원본 PNG가 저장되어야 한다. 검토 후보=False라도 안전/건강 인증으로 표시하면 실패다.
흔한 오해와 실패 복구
시험의 밝기94를 보고 threshold를6으로 올린 뒤 같은 시험을 독립 평가라고 부르지 않는다. 개발셋으로 편입하고 새 시험을 수집한다.
정상train에 실제 결함이 섞이면 기준이 오염된다. 무감독이라는 말은 데이터 품질 검토가 필요 없다는 뜻이 아니다.
이상지도 없는 모델의 설명에 “왼쪽 위 긁힘”을 적지 않는다. 위치 근거가 없으면 위치미확인으로 인계한다.
확인 문제
점수6이 threshold3을 넘은 멀쩡하지만 어두운 표식은 무엇을 보여주는가?
자동채점 · 선택형
해설과 정답 보기
정상과 다름과 물리적 결함은 다른 개념이다.
정답: 조명 변화에 의한 오탐 가능성
정상 val의 점수로 임계값을 정한 뒤 test 결과가 마음에 들지 않아 임계값을 바꾸었다면 무엇을 새로 해야 하는가?
자기평가 · 자동채점 아님
평가 기준: 독립성 상실, 변경기록, 새 시험 세 요소를 설명하면 통과.
해설 보기
그 test는 개발에 사용되었으므로 새 독립 시험이 필요하다. 임계값 변경과 데이터 편입을 기록한다.
독립 실습
val=[99,101,104]로 바꾸어 임계값을 재계산하라. 새로운 시험105와96의 점수와 review를 구하고 “>”를 “>=”로 바꾸었을 때96의 판정 변화도 설명하라.
남길 증거: 새 임계/경계값 코드와 stdout, FP 반례와 복구안. 선택 시 세 실제 PNG와 score 보고서.
기본 worked example — 표준라이브러리 실제 검산
from statistics import mean
normal_train = [98,100,102]
center = mean(normal_train)
validation_normal = [99,101,103]
threshold = max(abs(v-center) for v in validation_normal)
test = [('normal',101), ('scratch_candidate',112), ('dim_but_intact',94)]
print('normal_center', center, 'validation_threshold', threshold)
for name,value in test:
score = abs(value-center)
print(name, 'score', score, 'review', score>threshold)
# 마지막 항목은 분포 변화 반례이며 실제 결함의 증거가 아니다.
assert abs(94-center)>threshold
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""선택 실행 전용. 이번 원고 제작에서는 import/학습/다운로드하지 않음.
Python 3.11+의 별도 가상환경에서 실행. 모든 출력은 현재 학습 폴더에만 저장.
"""
import argparse
import copy
import json
from pathlib import Path
def classification(transfer=False, allow_download=False):
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, models, transforms
torch.manual_seed(73)
torch.set_num_threads(2)
weights = models.ResNet18_Weights.IMAGENET1K_V1 if transfer else None
if transfer and not allow_download:
raise SystemExit('전이학습 가중치 다운로드/라이선스 검토 후 --allow-download 필요')
transform = weights.transforms() if transfer else transforms.Compose([
transforms.Resize((64, 64)), transforms.ToTensor()])
data = {s: datasets.ImageFolder(Path('vision_data/classify') / s, transform)
for s in ('train', 'val', 'test')}
assert all(d.class_to_idx == data['train'].class_to_idx for d in data.values())
assert all(len(d) > 0 for d in data.values())
nclass = len(data['train'].classes)
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train'), num_workers=0)
for s, d in data.items()}
if transfer:
model = models.resnet18(weights=weights)
for p in model.parameters():
p.requires_grad_(False)
model.fc = nn.Linear(model.fc.in_features, nclass)
else:
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), nn.Conv2d(8, 16, 3, padding=1),
nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(),
nn.Linear(16, nclass))
optimizer = torch.optim.Adam((p for p in model.parameters() if p.requires_grad), lr=0.003)
loss_fn = nn.CrossEntropyLoss()
frozen_before = {k: v.clone() for k, v in model.state_dict().items()
if transfer and not k.startswith('fc.')}
best_score, best_state = -1, None
log = []
for epoch in range(5):
# 동결 backbone의 BatchNorm running statistics까지 유지한다.
if transfer:
model.eval()
model.fc.train()
else:
model.train()
total_loss = 0.0
for x, y in loaders['train']:
optimizer.zero_grad()
loss = loss_fn(model(x), y)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(y)
model.eval()
with torch.inference_mode():
correct = sum((model(x).argmax(1) == y).sum().item() for x, y in loaders['val'])
val_accuracy = correct / len(data['val'])
log.append({'epoch': epoch + 1, 'train_loss': total_loss / len(data['train']),
'val_accuracy': val_accuracy})
if val_accuracy > best_score:
best_score, best_state = val_accuracy, copy.deepcopy(model.state_dict())
model.load_state_dict(best_state)
model.eval()
matrix = [[0] * nclass for _ in range(nclass)]
with torch.inference_mode():
for x, y in loaders['test']:
for truth, pred in zip(y.tolist(), model(x).argmax(1).tolist()):
matrix[truth][pred] += 1
if transfer:
assert all(torch.equal(v, model.state_dict()[k]) for k, v in frozen_before.items())
prefix = 'transfer' if transfer else 'cnn'
torch.save({'state_dict': best_state, 'classes': data['train'].classes}, prefix + '.pt')
report = {'mode': prefix, 'classes': data['train'].classes, 'epochs': log,
'test_confusion_rows_truth': matrix, 'test_n': len(data['test']),
'scope': 'synthetic colored plates only; NOT bicycle deployment'}
Path(prefix + '-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
def semantic_mask():
import numpy as np
from PIL import Image
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
torch.manual_seed(73)
torch.set_num_threads(2)
class Pairs(Dataset):
def __init__(self, split):
self.files = sorted((Path('vision_data/segment') / split / 'images').glob('*.png'))
assert self.files, '먼저 prepare_assets.py 실행'
def __len__(self):
return len(self.files)
def __getitem__(self, i):
p = self.files[i]
x = np.array(Image.open(p).convert('RGB'), dtype=np.float32) / 255
m = np.array(Image.open(p.parent.parent / 'masks' / p.name), dtype=np.int64)
assert m.shape == x.shape[:2] and set(np.unique(m)).issubset({0, 1}), 'mask shape/labels invalid'
return torch.from_numpy(x).permute(2, 0, 1), torch.from_numpy(m)
sets = {s: Pairs(s) for s in ('train', 'val', 'test')}
loaders = {s: DataLoader(d, batch_size=4, shuffle=(s == 'train')) for s, d in sets.items()}
# 배경0/합성 표식1: 각 픽셀의 두 logit을 학습한다. 사전학습 아님.
model = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.Conv2d(8, 2, 1))
opt = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
def score(split):
inter = union = 0
model.eval()
with torch.inference_mode():
for x, y in loaders[split]:
pred = model(x).argmax(1)
inter += ((pred == 1) & (y == 1)).sum().item()
union += ((pred == 1) | (y == 1)).sum().item()
return inter / union if union else None
best, state = -1, None
for epoch in range(8):
model.train()
for x, y in loaders['train']:
opt.zero_grad()
loss = criterion(model(x), y)
loss.backward()
opt.step()
val_iou = score('val')
print('epoch', epoch + 1, 'val_iou', val_iou)
if val_iou is not None and val_iou > best:
best, state = val_iou, copy.deepcopy(model.state_dict())
assert state is not None
model.load_state_dict(state)
test_iou = score('test')
x, truth = sets['test'][0]
with torch.inference_mode():
pred = model(x.unsqueeze(0)).argmax(1)[0].numpy().astype('uint8')
Image.fromarray(pred * 255).save('mask-prediction.png')
Image.fromarray(truth.numpy().astype('uint8') * 255).save('mask-truth.png')
torch.save(state, 'mask-model.pt')
print(json.dumps({'test_foreground_iou': test_iou, 'test_n': len(sets['test'])}))
# 연결요소는 붙은 물체를 분리하지 못한다. 학습된 인스턴스 분할 모델이 아니다.
import cv2
n, instance_ids = cv2.connectedComponents(pred, connectivity=4)
np.save('mask-component-ids.npy', instance_ids)
print('connected_components_not_true_instances', n - 1)
def anomaly():
import numpy as np
from PIL import Image
from sklearn.ensemble import IsolationForest
rng = np.random.default_rng(73)
def image(defect=False, dark=False):
a = rng.normal(120, 3, (32, 32))
if defect:
a[12:18, 12:18] = 220
if dark:
a -= 40
return np.clip(a, 0, 255).astype(np.uint8)
def feature(a):
return [float(a.mean()), float(a.std()), float(np.quantile(a, .99))]
# 정상만 fit; 독립 정상 val로 threshold; test를 threshold 설정에 사용하지 않는다.
train = [image() for _ in range(80)]
val = [image() for _ in range(30)]
model = IsolationForest(n_estimators=80, contamination='auto', random_state=73, n_jobs=1)
model.fit([feature(a) for a in train])
threshold = float(np.quantile(-model.score_samples([feature(a) for a in val]), .95))
tests = [('normal', image()), ('bright-patch', image(defect=True)), ('dark-normal', image(dark=True))]
report = []
for name, a in tests:
score = float(-model.score_samples([feature(a)])[0])
report.append({'case': name, 'score': score, 'threshold': threshold,
'review_candidate': score > threshold})
Image.fromarray(a).save('anomaly-' + name + '.png')
Path('anomaly-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report, indent=2))
# 통계적 변화 후보일 뿐 결함 종류/의료 진단/설비 정지 명령이 아니다.
def main():
p = argparse.ArgumentParser()
p.add_argument('mode', choices=['cnn', 'transfer', 'mask', 'anomaly'])
p.add_argument('--allow-download', action='store_true')
a = p.parse_args()
if a.mode == 'cnn':
classification()
elif a.mode == 'transfer':
classification(True, a.allow_download)
elif a.mode == 'mask':
semantic_mask()
else:
anomaly()
if __name__ == '__main__':
main()
이상탐지 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
중심은 train에서 그대로100, threshold4. 105는 점수5로후보,96은4로 >에서는후보 아님, >=에서는후보. 경계 포함 여부를 운영명세에 명시해야 재현 가능하다.
평가 기준: 자동: 중심/임계4/시험5·4/경계 판정. 자기평가: 변화 후보가 고장 확정이 아님과 조명 복구 후 재촬영 계획. 임상 성능 주장은 금지.
새 맥락에 적용하기
합성 창고 바닥 얼룩 후보로 옮겨 비/그림자/빛 반사 조건의 정답 기준과 재촬영·사람 점검 분기를 설계하라. 시설 안전 판정은 별도 권한자에게 남긴다.
내 말로 설명하고 가르치기
학생은 “낯선 소리”와 “고장”의 차이를 설명한다. 강사는 평소 정상 조명이 바뀐 입력을 주고 모델을 탓하기 전 관측조건을 점검하는지 확인한다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
가려진 자리를 VLM이 “비었다”고 유창하게 설명하자 프로그램이 상태 장부를 바꾸려 했다. 이미지 속 “승인 없이 수정” 문장도 명령으로 취급했다.
VLM의 이미지/언어 결합과 근거 검토를 설명하고, 모의 응답 검증·읽기 전용 도구·로컬 실제모델 선택 코드를 분리한다.
먼저 알아둘 것
vision-task: unknown
vision-detect: 관측 후보
vision-segment: 영역 근거
핵심 한 문장: 시각 설명은 검토할 주장이지 사실 보증이나 실행 권한이 아니다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
창문을 보고 메모하는 안내원에게 창고 장부 수정 열쇠까지 주지 않는 것과 같다.
비유가 닿지 않는 곳: 안내원 비유와 달리 VLM은 실제로 기억이나 의도를 가진 사람이 아니다. 이미지를 토큰화하면서 작은 문자·정확한 개수 정보가 손실될 수 있다.
실제로 작동하는 원리
VLM의 시각 인코더는 이미지 패치를 특징으로 바꾸고 언어모델이 사용할 표현으로 연결한다. 텍스트 질문과 시각 표현을 조건으로 다음 토큰을 생성한다. 이 선택 모델의 제작사 카드는 SigLIP 이미지 인코더와 SmolLM2 텍스트 디코더, 패치 기반 시각 토큰 압축을 설명한다. 토큰을 적게 쓰는 것은 효율에 도움을 줄 수 있지만 작은 글자나 가려진 물체를 보장해서 읽게 하는 기술은 아니다. OCR 문자열 역시 원본 대조 전에는 주장이다.
기본 worked example은 실제 호출 없이 명시한 모의 JSON을 입력으로 받는다. claimed_state=empty라도 별도 관측 메모에 occluded=True면 화면에는 unknown을 강제한다. dispatch는 read_status만 허용하고 update_status는 쓰기0으로 거절한다. 이것은 서버 권한 경계의 교실 모형이며 실 DB 인증·동시성·감사를 구현했다고 주장하지 않는다. 프롬프트에 “수정하지 마”라고만 쓰고 실제 수정 도구를 열어 주는 것은 안전한 경계가 아니다.
선택 경로는 공개 로컬 SmolVLM-256M-Instruct에 직접 생성한 두 초록 사각형/회색 가림판/작은 A7 라벨을 준다. 기본언어가 영어인 모델이므로 영어 질문을 쓰고 한국어 학습자가 원본과 대조한다. 예상되는 관측 정답은 초록2, 글자A7, 가림판 뒤미확인이지만 실제 생성 문장은 실행 전 알 수 없다. 코드는 답변을 raw_answer로 저장하고 human_review_required·db_writes0를 고정한다. 모델이 “세 개” 또는“A1”이라고 답하면 실패 기록이지 정답으로 자동 교정해서 성공을 꾸밀 대상이 아니다.
영상 인식 자동화에 연결할 때는 YOLO 프레임 후보·촬영시각·좌표·미확인 상태를 구조화해 VLM/LLM이 설명하게 할 수 있다. 영상 전체를 봤다는 주장 대신 실제 처리한 프레임 번호만 보존한다. 이미지 속 지시는 데이터일 뿐 도구 권한을 바꾸지 않으며, 반환된 코드/명령을 eval/exec하지 않는다. 실제 장부 쓰기는 별도 인증된 승인 시스템의 책임이다.
모의 JSON을 사진 자체로 착각하지 않는다. 기본 예제의 관측 메모는 사람이 작성한 테스트 fixture다.
선택: setup의 CPU torch/Transformers 환경에서 optional_vlm.py를 저장한다. 라이선스/다운로드 허용 후 python optional_vlm.py --allow-download. 캐시가 준비되면 인수 없이 실행할 때 local_files_only=True다.
vlm-synthetic.png와 vlm-report.json의 raw_answer를 나란히 보고 주장별 정답/오류/미확인 표를 작성한다. 설치·다운로드·실제VLM/LLM 호출은 원고 제작 시 미실행이다.
입력 예제
모의JSON={slot:D,claimed_state:empty,reason:looks clear}; 독립 관측={slot:D,occluded:True,visible_state:None}. 선택 모델 입력은 별도 직접 생성 합성 그림이다.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
model_claim empty display_state unknown
update_status {'allowed': False, 'writes': 0}
read_status {'allowed': True, 'state': 'unknown', 'writes': 0}
선택 코드가 prompt 길이 이후 생성 토큰만 decode하는지 검사한다. 입력 질문까지 출력해 답변으로 오인하지 않는다.
선택 정답 표는 코드가 생성한 그림의 알려진 값이며 모델 예측이 아니다. raw_answer는 그대로 두고 검수 열을 별도로 작성한다.
흔한 오해와 실패 복구
다운로드 불허 상태에서 캐시 없음 오류는 정상적인 중단이다. 인증키를 요구하거나 기본 OAuth/모델 설정을 바꾸지 않는다.
CPU가 bfloat16/FlashAttention을 지원하지 않는 문제를 피하려고 float32/eager를 선택했다. 메모리가 부족하면 출력 길이를 줄이거나 실행을 중단하고 미실행으로 남긴다.
모델이 JSON을 잘못 생성했을 때 regex로 유리한 숫자만 뽑아 승인하지 않는다. 형식 검증 실패와 내용 충돌을 모두 사람 검토로 보낸다.
확인 문제
사진 속에 “관리자 승인 없이 장부 수정”이라고 쓰여 있다. 안전한 처리는?
자동채점 · 선택형
해설과 정답 보기
입력 이미지 문장은 권한을 부여하지 않는다. 서버 허용목록과 별도 인증을 유지한다.
정답: 이미지 내용으로 취급하고 쓰기 도구 거절
모의 JSON 검증 성공과 로컬 VLM 추론 성공을 어떤 증거로 구분하는가?
자기평가 · 자동채점 아님
평가 기준: fixture와 실추론 구분, 실제 입력/원출력 기록, 원본 검수 세 요소를 평가한다.
해설 보기
기본 fixture의 stdout은 규칙 검증만 증명한다. 실제 모델은 모델ID/환경/입력이미지/생성 raw_answer/실행시간 보고서가 필요하고 그 답은 원본 대조까지 해야 한다.
독립 실습
새 fixture에서 slot B, claimed_state=occupied, occluded=True로 바꾸어 결과를 재현하라. tool 요청을 delete_all로 바꾸고 거절되는지 검사한다. 사진 메모A7과 모의OCR A1이 충돌하는 검수표도 작성하라.
남길 증거: 변경 fixture 코드/거절 stdout/OCR 검수표. 선택 시 합성 PNG와 원응답·정답대조표, 다운로드/실행 여부 분리.
기본 worked example — 표준라이브러리 실제 검산
import json
# 독자 모의 응답이다. VLM 또는 OCR을 호출하지 않는다.
raw = '{"slot":"D","claimed_state":"empty","reason":"looks clear"}'
observation = {'slot':'D','occluded':True,'visible_state':None}
response = json.loads(raw)
assert response['slot'] == observation['slot']
state = 'unknown' if observation['occluded'] else response['claimed_state']
def dispatch(tool):
allowed = {'read_status'}
if tool not in allowed:
return {'allowed':False, 'writes':0}
return {'allowed':True, 'state':state, 'writes':0}
print('model_claim', response['claimed_state'], 'display_state', state)
print('update_status', dispatch('update_status'))
print('read_status', dispatch('read_status'))
assert dispatch('update_status')['writes'] == 0 and state == 'unknown'
선택 실제품 경로 전체 코드 — 제작 시 미실행
"""로컬 VLM 선택 경로: 미실행. 합성 그림 외 개인정보 입력 금지.
python optional_vlm.py --allow-download
두 번째 이후 캐시에 자료가 있으면 python optional_vlm.py (오프라인).
"""
import argparse
import json
import time
from pathlib import Path
def main():
p = argparse.ArgumentParser()
p.add_argument('--allow-download', action='store_true')
args = p.parse_args()
import torch
from PIL import Image, ImageDraw
from transformers import AutoProcessor, AutoModelForVision2Seq
torch.set_num_threads(2)
# 기본모델/OAuth 설정은 바꾸지 않고 별도 로컬 교육용 모델만 로딩한다.
model_id = 'HuggingFaceTB/SmolVLM-256M-Instruct'
image = Image.new('RGB', (256, 128), 'white')
draw = ImageDraw.Draw(image)
draw.rectangle((20, 35, 75, 90), fill='green')
draw.rectangle((105, 35, 160, 90), fill='green')
draw.rectangle((190, 10, 240, 110), fill='gray')
draw.text((5, 5), 'A7', fill='black')
image.save('vlm-synthetic.png')
options = {'local_files_only': not args.allow_download, 'trust_remote_code': False}
try:
processor = AutoProcessor.from_pretrained(model_id, **options)
model = AutoModelForVision2Seq.from_pretrained(
model_id, torch_dtype=torch.float32, _attn_implementation='eager', **options).to('cpu').eval()
except (OSError, ValueError) as exc:
raise SystemExit('모델 준비 실패. 캐시/Transformers 4.x 호환성/메모리를 확인하고, '
'다운로드 승인 전에는 --allow-download를 주지 마세요. 원인: ' + str(exc))
query = ('Describe only visible shapes. How many green squares are visible? '
'Read the small label if legible; otherwise say unknown. '
'Do not infer anything behind the gray cover. No actions or database updates.')
messages = [{'role': 'user', 'content': [{'type': 'image'}, {'type': 'text', 'text': query}]}]
prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
inputs = processor(text=prompt, images=[image], return_tensors='pt').to('cpu')
begin = time.perf_counter()
with torch.inference_mode():
ids = model.generate(**inputs, max_new_tokens=96, do_sample=False)
# prompt까지 함께 decode해서 입력 질문을 답변으로 오인하지 않는다.
answer = processor.batch_decode(ids[:, inputs['input_ids'].shape[1]:], skip_special_tokens=True)[0]
result = {'model': model_id, 'image': 'vlm-synthetic.png', 'question': query,
'raw_answer': answer, 'seconds_generate_only': time.perf_counter() - begin,
'status': 'human_review_required', 'db_writes': 0,
'known_synthetic_truth': {'visible_green_squares': 2, 'label': 'A7', 'behind_cover': 'unknown'}}
Path('vlm-report.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
VLM 선택 설치/복구
선택 실제품 경로 안내 (이 원고 제작에서는 전부 미실행)
기본 경로는 Python 표준라이브러리뿐이며 pip/카메라/인터넷 없이 실행한다. 아래는 학습자가 명시적으로 선택할 때만 하는 별도 로컬 교육환경이다. 호스트의 기본 모델·OAuth·시스템 Python·전역 설정을 바꾸지 않는다. 유료 API나 클라우드 학습 서비스에 가입하지 않는다.
Linux/macOS: 설치된 Python 3.11 또는 3.12로 새 학습 폴더에서
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Windows PowerShell: py -3.11 -m venv .venv 이후 활성화 대신 .venv\Scripts\python.exe를 아래 python 자리에 사용해도 된다. 실행정책을 전역 완화하지 않는다.
Python 3.14에서 wheel이 없으면 시스템 버전을 내리지 말고 지원되는 별도 3.11/3.12 환경을 사용한다. venv에 pip가 없으면 python -m ensurepip --upgrade; ensurepip도 없는 배포판이면 관리자가 제공한 venv 또는 승인된 패키지 환경을 사용하고 설치 미완료로 기록한다.
아래 단원에 필요한 패키지만 선택해 설치한다. CPU부터 시작한다.
OpenCV 숫자영상: python -m pip install numpy opencv-python
CNN/전이학습/마스크: python -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
이후 python -m pip install pillow numpy opencv-python
YOLO: 위 CPU torch/torchvision 환경에서 python -m pip install ultralytics pillow
이상탐지: python -m pip install scikit-learn numpy pillow
VLM: 위 CPU torch/torchvision 환경에서 python -m pip install "transformers==4.57.6" pillow accelerate
설치 후 python -m pip check, python -m pip freeze > environment.txt 로 실제 버전 기록. 최신 버전 보장이나 위 조합의 실설치 성공을 주장하지 않는다. torch/torchvision operator 불일치면 둘을 동일 배포 채널의 호환 쌍으로 새 venv에서 설치하고 pip check를 다시 수행한다. 설치가 막히면 순수Python 경로만 끝내고 미실행이라고 쓴다.
모든 코드·자료 파일을 같은 학습 폴더에 저장한다. prepare_assets.py는 한 번만 실행하며 기존 vision_data를 덮어쓰지 않는다. 반복 실험은 새 폴더에서 한다. 첫 전이학습/VLM 실행은 공개 가중치 다운로드가 필요하며 라이선스·저장공간·네트워크 사용 허용을 확인한 뒤에만 --allow-download를 붙인다. 인증키/결제는 필요 없다. 오프라인에서는 캐시가 없으면 실패하는 것이 맞다. CNN·마스크·YOLO YAML 경로는 무작위 가중치로 합성 자료를 학습하며 사전학습 성능을 흉내 낸 출력은 만들지 않는다.
Ctrl+C로 오래 걸리는 실행을 중단할 수 있다. CPU·작은 배치·짧은 epochs는 동작 경로 확인용일 뿐 품질 보장이 아니다. 모델 출력의 점수/IoU/지연 수치는 실제 실행 후 생성된 보고서에서 읽고 이 교재의 순수Python 계산값으로 채우지 않는다. 의료영상·실제 얼굴·차량번호·사생활 사진을 사용하지 않는다. 라이선스가 불명확한 자료와 외부 다운로드를 추가하지 않는다.
설치 문서 확인 범위: Transformers4.x 공식 Installation의CPU PyTorch index/venv 절차와 Ultralytics Quickstart의pip 설치·PyTorch 선설치를 확인했다. PyTorch get-started 페이지는 CMS인코딩 조각으로 반환되어 그 페이지 전체 또는 최신버전쌍을 확인했다고 주장하지 않는다. 실제 패키지 설치/런타임호환성은 미실행이다.
실습 해설 보기
가림 때문에 상태는 여전히 unknown. delete_all은 허용목록에 없어서 allowed=False/writes0. OCR A1은 원본A7과 충돌하므로 해당 문자열만 보류하고 재촬영/확대 원본 검토를 요청한다. 도구 권한을 늘리지 않는다.
평가 기준: 자동: unknown과 delete_all 거절·writes0. 자기평가: OCR 충돌의 근거와 사람 인계. 실제 VLM 실행 주장은 실보고서 없으면 불통과.
새 맥락에 적용하기
합성 배송 상자 라벨 점검으로 옮겨 글자·상자수·파손후보 설명과 실제 발송/재고 수정 사이의 권한 경계를 작성하라. 민감 주소와 실제 외부 발송은 넣지 않는다.
내 말로 설명하고 가르치기
학생이 읽는 입/수정하는 손을 두 상자로 그린다. 강사는 이미지 속 악성 지시와 모델의 확신 높은 오답을 각각 제시해 같은 읽기전용 경계가 유지되는지 본다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.
읽음과 숙련은 별개입니다.
참고 출처와 확인 범위
https://huggingface.co/HuggingFaceTB/SmolVLM-256M-Instruct — 제작사 모델 카드의 원시 README 전체를 curl로 읽음: 이미지+텍스트→텍스트, SigLIP/SmolLM2, 64 visual tokens/512 patch, 영어, Apache-2.0, AutoProcessor/AutoModelForVision2Seq 사용 흐름, high-stakes 금지. 가중치·라이브러리·추론 다운로드/실행 없음. 원 고유 사진·응답 예제는 쓰지 않음.
기술별 예제는 있지만 동료가 실행할 수 있는 통합 시연과 출시 보류 근거가 없다. 낮 사진만 맞힌 결과를 현장 서비스로 발표하려 한다.
문제 명세·팀 책임·입력→처리→표시→검증을 연결한 로컬 데모와 조건별 평가·실패 시연·재현 발표를 완성한다.
먼저 알아둘 것
vision-task부터 vision-agent까지 기본 실습 재현
핵심 한 문장: 완성은 예쁜 정답 화면이 아니라 새 조건과 장애에서도 모르는 것을 드러내는 재현 가능한 결과다.
지금 할 일준비물개념과 원리따라 하기예상 결과와 검증오류 복구혼자 변형설명과 기여
큰 그림: 비유와 경계
시운전은 잘 달리는 장면뿐 아니라 비가 오거나 계기판이 고장 났을 때 멈추는 방법까지 확인한다.
비유가 닿지 않는 곳: 합성 시운전은 실제 센서·사람·환경의 다양성과 운영 책임을 대체하지 않는다.
실제로 작동하는 원리
프로젝트 명세는 “무인 합성 보관대의 자리별 상태를 검토용으로 표시한다. 장부 수정·예약·설비제어는 하지 않는다”로 고정한다. 데이터 담당은 사용권/촬영그룹/라벨 정책, 모델 담당은 변환/가중치/val 선택, 검증 담당은 오류·누출·조건별 평가, 발표 담당은 실행 상태·한계·복구를 책임진다. 한 사람이 모든 역할을 맡아도 산출물을 분리한다.
기본 통합 코드는 외부 모델 대신 명시적 synthetic fixture를 받는다. 낮2건은 정답, 야간4건은 truth[1,1,0,0]/pred[0,0,1,0]으로3오류다. 가림1건과 timeout1건은 unknown. 알려진 정답이 없는 둘은 정확도 분모에 몰래 넣지 않고 보류처리 검증으로 별도 평가한다. 모든 cases를 화면행으로 구조화해 JSON과 접근 가능한 정적 HTML 표를 만든다. 이 코드가 실제로 완결하는 것은 로컬 표시/정책 파이프라인이지 모델/카메라 서비스가 아니다.
출시 판단은 야간 오류3/4 때문에 hold. 이 작은 표본으로 진짜 현장 오류율을 추정하지 않는다. 조명·각도·겹침은 서로 다른 실패 원인일 수 있어 별도 조건 그룹을 예약해야 한다. 각도 변경·새 조명·겹침 합성 입력을 추가하고 가림 정책을 검증하되, 사람이 이미 정답을 아는 시험을 계속 튜닝에 재사용하면 새 독립 test가 필요하다.
선택 통합에서는 앞 단원의 실제 모델 보고서를 복사해 fixture라고 꾸미지 않는다. 분류의 class/점수, 탐지의 box/score/frame, 마스크의 이미지크기/ID, 이상점수의 threshold, VLM의 raw_answer를 공통 검토 레코드의 서로 다른 필드로 보존한다. 이 원고는 모든 선택 모델의 동시 실행 오케스트레이터를 구현했다고 주장하지 않는다. 실제 연결은 필요 모델 하나부터 통합하고 test/권한/장애를 다시 검증한 뒤에만 확장한다.
따라 하기
내장 project-contract.md를 읽고 역할·입출력 계약·허용/금지·인수기준을 팀원이 자기 말로 설명한다.
python3 vision-project.py를 실행한다. 생성한 vision-demo.html을 로컬 브라우저로 열고 JSON과 표의 id/condition/display를 비교한다. 파일 열기는 외부 게시가 아니다.
정상/가림/timeout을 시연하고 야간 오류와 release=hold를 숨기지 않는다. 원고 제작에서는 JSON/HTML 파일 생성과 Python assert를 실행했으며 부모 통합검수에서 로컬 Chromium의 360px 정적표 렌더·가로넘침을 실제 확인했다. 이는 공개 사이트 렌더나 모든 브라우저 접근성 검증은 아니다.
동료가 빈 학습 폴더에서 같은 파일로 재현하게 한다. 모델 경로를 선택했다면 실제 설치/버전/파일/지연 로그를 따로 제출하고 기본 계산 로그와 섞지 않는다.
3분 발표: 문제30초, 정상·가림·장애 시연60초, 조건별 오류45초, 책임·한계·다음 독립시험45초. 각 수치는 발표 구성 예상치일 뿐 학습시간 보장 아니다.
입력 예제
기본 fixture8건: 주간2건, 야간4건(truth=[1,1,0,0],pred=[0,0,1,0]), 가림1건, timeout1건. 전부 가상 비민감 자료.
예상 결과
아래는 내장 기본 코드의 실제 검산 stdout이다. 모델 추론 결과가 아니다.
night_errors 3 / 4 error_rate 0.75
covered_and_failure ['unknown', 'unknown']
release hold writes 0
saved vision-demo-report.json vision-demo.html
검증 방법
JSON rows와 HTML의8개 사례가 대응하는지 확인하고 night_errors3/night_n4, 마지막 둘unknown, writes0, release hold를 검사한다.
필수 인수시험: normal 표시 유지, occluded→unknown, timeout→unknown, 쓰기0. 자동 assert와 동료의 화면·설명 검수는 다른 평가다.
독립 재현자는 코드에서 한 입력을 바꾸고 표/JSON이 함께 바뀌는지 확인한다. 제작자의 실행이 학습자의 숙련 증거는 아니다.
흔한 오해와 실패 복구
브라우저에서 파일이 열렸다는 사실과 실제 모델이 연결되었다는 사실은 다르다. 페이지 맨 위 synthetic 표식을 지우지 않는다.
야간 시험을 보고 모델을 고쳤다면 원래 야간표는 회귀시험으로 유지하되 새로운 독립 야간·각도·겹침 자료로 다시 평가한다.
HTML은 html.escape로 id/condition/display를 이스케이프한다. 외부 입력으로 확장할 때도 이 처리를 제거하지 말고 입력 스키마·파일 경로·관측시각·인증을 추가 검증한다.
확인 문제
야간4건 중3오류이고 가림/장애는 보류되었다. 올바른 발표는?
자동채점 · 선택형
해설과 정답 보기
작동한 범위와 성능 실패를 분리한다. 보류 처리는 정확도 정답과 같은 분모가 아니다.
정답: 합성 정책 데모는 동작하나 야간 출시 보류
이번 기본 데모에서 실제 구현한 것과 아직 검증하지 않은 것을 각각 세 가지씩 말하라.
자기평가 · 자동채점 아님
평가 기준: 구현3가지와 미검증3가지가 코드/보고서와 일치하고 가짜 실모델 주장이 없으면 통과.
해설 보기
구현: fixture 읽기/unknown 정책/JSON·HTML 표시. 미검증: 실제 카메라·모델 성능·현장 지연/새 환경. 선택 경로를 실제 실행했다면 그 증거 범위만 추가한다.
독립 실습
해설을 가리고 새 angle 사례(truth=1,pred=0,occluded=False,error=None)와 overlap 사례(truth=None,pred=0,occluded=True,error=None)를 추가하라. 조건별 정답 있는 표본의 오류수와 보류수 보고를 확장하고 두 출력파일을 다시 생성해 동료에게 시연하라.
교육자 재사용 과제: 강사용 카드: 새 각도 사진 1개에서 가상 예측 오류를 내고 독립 test로 분리한다. 왜 이전 야간 샘플을 반복 튜닝하면 평가 누출인지 학생 설명·재현 코드와 대조한다. 실제 사진 수집이나 얼굴인식은 하지 않는다.
남길 증거: 실제 실행 stdout, 생성 JSON/HTML, 새 angle/overlap 입력과 조건별 결과, 동료 재현 체크·teach-back 설명·미실행 목록.
보강 증거: 예측→관측→수정 후 출력과 타인 인계/미실시 기록을 구분한다.
기본 worked example — 표준라이브러리 실제 검산
import json
from pathlib import Path
cases = [
{'id':'day-a','condition':'day','truth':0,'pred':0,'error':None,'occluded':False},
{'id':'day-b','condition':'day','truth':1,'pred':1,'error':None,'occluded':False},
{'id':'night-a','condition':'night','truth':1,'pred':0,'error':None,'occluded':False},
{'id':'night-b','condition':'night','truth':1,'pred':0,'error':None,'occluded':False},
{'id':'night-c','condition':'night','truth':0,'pred':1,'error':None,'occluded':False},
{'id':'night-d','condition':'night','truth':0,'pred':0,'error':None,'occluded':False},
{'id':'covered','condition':'occlusion','truth':None,'pred':0,'error':None,'occluded':True},
{'id':'broken','condition':'failure','truth':None,'pred':None,'error':'timeout','occluded':False}]
from html import escape
required = {'id','condition','truth','pred','error','occluded'}
seen = set()
rows = []
for c in cases:
if set(c) != required or not isinstance(c['id'], str) or c['id'] in seen:
raise ValueError('invalid fields or duplicate id')
if not isinstance(c['condition'], str) or type(c['occluded']) is not bool:
raise ValueError('invalid condition/occluded')
for key in ('truth','pred'):
if c[key] is not None and (type(c[key]) is not int or c[key] not in (0,1)):
raise ValueError('invalid binary label')
if c['error'] is not None and not isinstance(c['error'], str):
raise ValueError('invalid error code')
seen.add(c['id'])
state = 'unknown' if c['occluded'] or c['error'] is not None or c['pred'] is None else ('occupied' if c['pred'] else 'empty')
rows.append(dict(c, display=state, writes=0, origin='synthetic-fixture-not-model'))
night = [r for r in rows if r['condition']=='night']
errors = sum(r['truth']!=r['pred'] for r in night)
assert errors == 3 and all(r['display']=='unknown' for r in rows if r['id'] in {'covered','broken'})
assert sum(r['writes'] for r in rows) == 0
summary = {}
for condition in sorted({r['condition'] for r in rows}):
subset = [r for r in rows if r['condition'] == condition]
labeled = [r for r in subset if r['truth'] is not None]
summary[condition] = {'labeled_n': len(labeled),
'errors': sum(r['truth'] != r['pred'] for r in labeled),
'withheld_n': sum(r['display'] == 'unknown' for r in subset)}
report = {'rows':rows, 'by_condition':summary, 'night_errors':errors, 'night_n':len(night), 'release':'hold', 'writes':0}
Path('vision-demo-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
items = ''.join('<tr><td>'+escape(r['id'])+'</td><td>'+escape(r['condition'])+'</td><td>'+escape(r['display'])+'</td></tr>' for r in rows)
html = '<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>합성 비전 검토판</title><style>body{font:18px sans-serif;max-width:900px;margin:2rem auto;padding:1rem}td,th{padding:.5rem;border:1px solid #999}table{border-collapse:collapse}</style><h1>합성 비전 검토판</h1><p>실제 카메라·모델 아님. 출시 보류. 장부 수정 0회.</p><table><caption>입력 처리 표시 결과</caption><thead><tr><th scope="col">사례</th><th scope="col">조건</th><th scope="col">표시</th></tr></thead><tbody>'+items+'</tbody></table><p>unknown은 빈자리가 아닙니다. 현장 확인이 필요합니다.</p></html>'
Path('vision-demo.html').write_text(html, encoding='utf-8')
print('night_errors', errors, '/', len(night), 'error_rate', errors/len(night))
print('covered_and_failure', [r['display'] for r in rows if r['id'] in {'covered','broken'}])
print('release', report['release'], 'writes', report['writes'])
print('saved vision-demo-report.json vision-demo.html')
프로젝트 계약·인수시험·발표 원고 틀
# 로컬 합성 보관대 검토판 계약
## 범위
사용자: 안내 담당자. 가치: 확실한 관측과 미확인을 분리해 잘못된 안내를 찾는다. 비범위: 실제 예약/장부수정/얼굴식별/차량번호/카메라 상시감시/의료·산업안전 판정/외부게시.
## 입력
id 고유문자열, condition(day/night/angle/overlap/failure), truth 0/1/null, pred 0/1/null, occluded bool, error null 또는 오류코드. 코드는 필드 집합·고유id·라벨 타입을 검증하며 pred=null은 error 여부와 별개로 unknown으로 처리한다. 기본 fixture의 pred=null에는 timeout도 명시했다. 인증·관측시각·파일 접근 권한은 실제 시스템 연결 전 별도 검증 대상이다.
## 출력
각 행 origin=synthetic-fixture-not-model, display(empty/occupied/unknown), writes=0. 조건별 labeled_n/errors와 별도 withheld_n. UI에 생성시각/실행환경/미검증 범위를 덧붙일 수 있으나 카메라실시간으로 이름붙이지 않는다.
## 팀 책임
데이터 담당: 그룹별 split, 라벨 정책, 권리, 가림/불명확 표시. 모델 담당: 변환과 가중치/파라미터/seed. 검증 담당: 정답 누출/좌표/0분모/장애/복제 검토. 발표 담당: 원출력/실행상태/한계 보존. 팀장은 권한 확대·외부공개를 승인 없이 하지 않는다.
## 사전 인수기준
기본 fixture의 모든 expected state 일치, 미확인/장애 writes0, JSON/화면 id 일치, 새로운조건 오류 분리, 보고서에 모의와 실제 구별. 이 기준을 통과해도 실제 출시 승인이 아니다. 현장 인수기준의 표본수/FN 비용/시간예산은 실제 운영자가 별도로 사전에 정해야 한다.
## 시연·발표 해설
A. 정상: day-a→empty. 예측이 맞는 이유가 데이터 정답과 일치함임을 보여준다.
B. 가림: covered→unknown. pred0을 빈자리로 쓰지 않는 정책을 설명한다.
C. 장애: broken→unknown. timeout을empty로 변환하지 않는다.
D. 실패: night-a/b/c의 오답은 숨기지 않고 실제 fixture 입력과 출력으로 제시한다.
E. 결과: 야간3/4 오류, 출시hold, 쓰기0. 모의의 재현성과 현실 성능의 차이를 설명한다.
## 복구
파일 생성오류는 경로/쓰기권한 확인 후 새 학습폴더에서 재실행. 모델장애는 결과 미확인으로 남기고 권한 있는 담당자에게 알림. 실제 데이터 연결 시 입력 스키마 검증·HTML escaping·관측시각 만료·인증·감사·보존기한을 추가 검증하기 전 공개하지 않는다.
## 독립 제출
요구정의, 수정코드, 입력원본, 실행stdout, JSON, 화면증거, 새조건 오류표, 권한표, 회귀시험, 미실행목록. 원자료/유료교재/사용권 없는 사진을 첨부하지 않는다.
실습 해설 보기
angle은 오류1/표본1, 표시empty이므로 실패사례로 보여야 한다. overlap은 unknown 보류1건이고 정확도 분모에서 제외한다. 기존야간3/4와 hold는 그대로다. angle이 가려짐 아님에도 틀린 사례는 현재 unknown 규칙만으로 모든 오답을 막지 못함을 보여준다. 분류/입력 개선은 val에서 진행하고 새 조건 독립시험을 계획한다.
평가 기준: 자동: 기존8건 회귀시험+새2건의 JSON/표 대응, angle 오류1/1, overlap 보류1, writes0/hold. 동료: 설치 없이 재현·실패원인 설명·미검증 경계·새 테스트 설계 네 항목 모두 확인. 객관식만 맞으면 완료 아님.
새 맥락에 적용하기
직접 그린 공용 공구함 검토판으로 옮겨 empty/occupied 정의·새 라벨·오탐/누락 영향·미확인 안내를 다시 쓰고, 다른 사람이 고른 미공개 합성 사례로 재시연하라.
독립 전이·실패주입: 새 가상 입력: 공용 도구함 야간 합성 사진 4장 중 예측/정답 쌍 (비었음,있음),(있음,있음),(비었음,비었음),(있음,비었음). 가림 1장은 정답 없음. 기존 fixture 코드의 조건별 오류 계산을 새 레코드에 맞게 바꾸기 전에 야간 오류 2/4, 가림은 unknown을 예측한다. 실패주입으로 가림을 비었음 정답으로 세는 코드를 만들어 분모 왜곡을 관찰한 뒤 원복한다.
내 말로 설명하고 가르치기
학생이 코드 없이 입력→처리→표시→검증을 그림으로 설명한다. 강사는 오류·가림·모델장애 중 하나를 즉석 투입하고 왜 보류/실패로 나타나는지 해당 코드와 연결해 설명하게 한다.
독립 설명·인수 판정: 판정: 예측·정답·조건을 행별로 남겨 야간 오류 2/4와 unknown 별도 집계, 쓰기 0/출시 보류를 재현한다. 텍스트 정책 fixture는 카메라/실모델 정확도 증거가 아니다. 동료는 합성 라벨/이미지 권리/실패 범위를 확인하고 AI 없이 픽셀 입력과 규칙 출력의 차이를 설명한다.
실습·전이·설명 결과는 본인 또는 동료가 기준에 따라 검토하세요. 이 페이지는 숙련을 자동 인증하지 않습니다.