← 모델 목록
수업 실습 2026-10-08 · gesture-classification
트리 앙상블 계열 (랜덤 포레스트)

RandomForestClassifier (scikit-learn)

랜덤 포레스트 가위바위보 분류 (MediaPipe 손 좌표, 수업 실습)
모델
결정 트리 여러 개의 투표로 분류하는 scikit-learn 앙상블 모델. 여기서는 MediaPipe가 뽑은 손 좌표 63개를 입력으로 받아 바위·가위·보를 분류
사용 목적
웹캠으로 직접 모은 손 좌표 데이터로 가위바위보 분류기를 학습하고, 저장한 모델을 실시간 웹캠 예측에 쓰는 흐름을 익히려고 사용
코드
mnmms/data-analysis
가중치
rock_scissors_paper.pkl 0.35MB · 로컬 보관
models/rock_scissors_paper.pkl (노트북 폴더 기준)
accuracy96.0%
macro_f195.0%
학습 시간없음
추론 CPU / GPU측정 안 함

데이터

  • 직접 수집 347장 · Rock 167 / Scissors 120 / Paper 60
  • MediaPipe 손 좌표 21개 × XYZ = 63개 특징
  • train 277 · test 70 (8:2, seed 42)

학습

  • 처음부터 학습 · n_estimators 100
  • CPU · scikit-learn 1.9.0
  • 학습 시간 [미확인]

결과

  • test accuracy 0.96 · macro F1 0.95
  • Rock 0.99 · Scissors 0.93 · Paper 0.92 (F1)

배운 점

  • Paper 재현율이 0.86으로 가장 낮음
  • 클래스 수가 달라 Paper 샘플이 가장 적음 (60장)
  • joblib으로 저장해 실시간 예측 스크립트에서 불러 씀
개요용도 의도한 용도: 이미지 대신 손 좌표(특징)를 뽑아 가벼운 전통 ML 모델로 분류하는 흐름 익히기 (수집 → 학습 → 저장…

용도

  • 의도한 용도: 이미지 대신 손 좌표(특징)를 뽑아 가벼운 전통 ML 모델로 분류하는 흐름 익히기 (수집 → 학습 → 저장 → 실시간 예측)
  • 대상 사용자: 해당 없음 (학습용)
  • 의도하지 않은 용도: 일반 사용자의 손 제스처 인식. 한 사람의 웹캠 환경에서 모은 347개로만 학습·평가함

모델 정보

항목 값
베이스 모델 직접 구현 (처음부터 학습). 특징 추출은 MediaPipe hand_landmarker.task (float16, .../hand_landmarker/float16/1/, 04_3_mediapipe_download.ipynb로 다운로드)
크기 · 변형 RandomForestClassifier(n_estimators=100, random_state=42). 나머지 하이퍼파라미터는 scikit-learn 기본값. 파일 크기 0.35MB
리비전 scikit-learn 1.9.0, joblib 1.5.3, mediapipe 1.1.0 (uv.lock 기준)
아키텍처 입력 63차원(손 랜드마크 21개 × x, y, z) → 결정 트리 100개의 투표 → 3개 클래스 (0: Rock, 1: Scissors, 2: Paper)
학습 방식 처음부터 학습 (지도학습, 사전학습 가중치 없음)
프롬프트 · 에이전트 구성 해당 없음
데이터확보 방식 직접 수집 출처 웹캠으로 직접 만든 data/hand_data.csv (열 이름 없음, 64열 = 좌표 63 + 라…
항목 값
확보 방식 직접 수집
출처 웹캠으로 직접 만든 data/hand_data.csv (열 이름 없음, 64열 = 좌표 63 + 라벨 1)
직접 수집 내용 04_5_mediapipe_collect_data.py: 웹캠(좌우 반전) 프레임에서 MediaPipe HandLandmarker(num_hands=1, 감지·존재·추적 confidence 0.5)가 찾은 손 좌표 21개(x, y, z)를 모음. 키보드 1/2/3을 누르는 순간의 좌표 한 줄을 Rock / Scissors / Paper 라벨과 함께 CSV에 추가(append). 라벨은 누른 키로 직접 붙임. 수집 기간·수집한 사람 수·촬영 환경 [미확인] (CSV에 시각 정보가 없음)
이용 조건 확인 해당 없음 (외부 사이트가 아니라 본인 웹캠으로 수집). 영상 프레임은 저장하지 않고 좌표만 저장
규모 347개 (노트북 data.shape = (347, 64))
분할 train 277 / test 70, train_test_split 비율 8:2, 시드 42, shuffle. validation 분할 없음
레이블·클래스 분포 Rock 167 (48.1%) / Scissors 120 (34.6%) / Paper 60 (17.3%). test 지원 수는 Rock 35 / Scissors 21 / Paper 14 (노트북 출력은 분포를 직접 보여 주지 않아 CSV를 세어 확인)
전처리 열 이름 지정(x1, y1, z1 … x21, y21, z21, label), 라벨 → 숫자 (Rock=0, Scissors=1, Paper=2, 예전 오타 표기 Sissors도 1로 매핑). 좌표 정규화·스케일링 없음 (MediaPipe가 주는 정규화 좌표 그대로)
개인정보·민감정보 처리 손 좌표 숫자만 저장하고 영상·이미지는 저장하지 않음. 이 레포에도 원본 CSV는 넣지 않음
학습하이퍼파라미터 n_estimators=100 , random_state=42 , 그 외 기본값 에폭 / 스텝 해당 없음 (트리…
항목 값
하이퍼파라미터 n_estimators=100, random_state=42, 그 외 기본값
에폭 / 스텝 해당 없음 (트리 100개를 한 번에 fit)
학습 시간 [미확인] (노트북에 시간 측정이 없음)
하드웨어 CPU (scikit-learn은 GPU를 쓰지 않음). 장비는 이 컴퓨터의 Intel Core Ultra 9 185H로 보이나 이 노트북에서 직접 확인하지는 않음 [미확인]
프레임워크 scikit-learn 1.9.0, pandas 3.0.5, joblib 1.5.3 (uv.lock 기준)
시드 · 재현 방법 train_test_split(test_size=0.2, random_state=42, shuffle=True), 모델 random_state=42. 노트북을 위에서부터 실행 (data/hand_data.csv 필요)
결과테스트셋(70개, 전체의 20%)에서 classification_report 결과입니다. Rock 1.00 0.97 0.99 …

테스트셋(70개, 전체의 20%)에서 classification_report 결과입니다.

클래스 precision recall f1-score support
Rock 1.00 0.97 0.99 35
Scissors 0.88 1.00 0.93 21
Paper 1.00 0.86 0.92 14
accuracy 0.96 70
macro avg 0.96 0.94 0.95 70
weighted avg 0.96 0.96 0.96 70
  • 비교 대상 (baseline 등): 없음. 다른 모델과 비교하지 않았습니다.
  • [🚩AI 설명] 리포트의 recall·precision으로 되짚으면 오분류는 3개로 보임: Rock 1개와 Paper 2개가 Scissors로 예측됨 (Scissors precision 0.88 = 21/24). 혼동행렬은 노트북에서 출력하지 않아 직접 확인하지는 않음.

추론 성능 (CPU / GPU)

측정 안 함. 지연 시간·처리량·메모리를 재는 코드가 노트북에 없습니다. infer_ms는 넣지 않았습니다. 노트북은 저장한 모델로 테스트 샘플 1개를 예측해 Rock이 나오는 것만 확인했습니다.

장비 장비 이름 배치 크기 지연 시간 (샘플당, p50 / p95) 처리량 (샘플/초) 메모리 사용
CPU [미확인] 1 측정 안 함 측정 안 함 측정 안 함
GPU 해당 없음 (scikit-learn은 CPU 전용) - - - -
한계 · 배운 점한계 알려진 실패 유형: 테스트에서 Paper의 재현율이 0.86으로 가장 낮고 Scissors의 precision이 0.88…

한계

  • 알려진 실패 유형: 테스트에서 Paper의 재현율이 0.86으로 가장 낮고 Scissors의 precision이 0.88로 낮음. 노트북에서 오분류 샘플을 직접 확인하지는 않음 [미확인]
  • [🚩AI 설명] 한 사람이 같은 자세로 키를 연달아 눌러 모았다면 이웃한 샘플이 거의 같은 값일 수 있어, 무작위로 나눈 test에도 train과 비슷한 샘플이 섞여 점수가 높게 나왔을 수 있음. 수집 방식(연속 저장 여부)은 [미확인]
  • [🚩AI 설명] 클래스 불균형(Paper 60개)이 있는데 class_weight를 쓰지 않았고, test가 70개라 샘플 1~2개로 지표가 크게 움직임
  • 편향 / 공정성 이슈: [미확인] (수집한 사람 수·손 크기·피부색·조명 다양성을 확인하지 않음)
  • 성능이 떨어지는 입력 조건: [미확인] (카메라 각도·거리·왼손/오른손별 평가를 하지 않음. 수집·예측 모두 num_hands=1)

배운 점

  • 이미지 모델을 학습하지 않아도, 랜드마크 좌표(63개 숫자)만 있으면 작은 표 형태 데이터로 RandomForest가 바로 분류할 수 있다 (347개로 test accuracy 0.96)
  • 수집 CSV에는 열 이름이 없어 header=None으로 읽고 열 이름을 직접 붙여야 한다
  • 학습 때와 예측 때 같은 형태(NumPy 배열, 같은 63개 순서)로 넣어야 한다. 노트북도 to_numpy()로 맞춤
  • joblib.dump / joblib.load로 저장한 모델을 실시간 웹캠 예측 스크립트(04_7_mediapipe_pred.py)에서 불러 쓴다
  • [🚩AI 설명] 다음에 해 볼 것: 혼동행렬 확인, 사람·조명·거리를 바꿔 모은 데이터로 따로 평가, 클래스 균형 맞추기, 손목 기준 좌표 변환 같은 정규화 전처리 비교
사용 방법코드 셀 6개
In [1]데이터 불러오기
import pandas as pd

# 수집한 CSV에는 열 이름이 없습니다.
data = pd.read_csv("data/hand_data.csv", header=None)
data.shape
Out[1] 출력 보기
(347, 64)
In [2]라벨을 숫자로 바꾸기
# 바위=0, 가위=1, 보=2 (기존 Sissors 표기도 포함)
label_map = {"Rock": 0, "Scissors": 1, "Sissors": 1, "Paper": 2}
data["label_enc"] = data["label"].map(label_map)
data[["label", "label_enc"]].head()
In [3]학습·평가 데이터 나누기
from sklearn.model_selection import train_test_split

# X는 손 좌표, y는 정답 라벨입니다.
X = data[columns[:-1]].to_numpy()
y = data["label_enc"].to_numpy()

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, shuffle=True
)
print("학습 데이터:", X_train.shape, y_train.shape)
print("평가 데이터:", X_test.shape, y_test.shape)
Out[3] 출력 보기
학습 데이터: (277, 63) (277,)
평가 데이터: (70, 63) (70,)
In [4]모델 학습하기
from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)
In [5]모델 성능 평가하기
from sklearn.metrics import classification_report

y_pred = model.predict(X_test)
print(classification_report(
    y_test, y_pred,
    labels=[0, 1, 2],
    target_names=["Rock", "Scissors", "Paper"],
    zero_division=0
))
Out[5] 출력 보기
              precision    recall  f1-score   support

        Rock       1.00      0.97      0.99        35
    Scissors       0.88      1.00      0.93        21
       Paper       1.00      0.86      0.92        14

    accuracy                           0.96        70
   macro avg       0.96      0.94      0.95        70
weighted avg       0.96      0.96      0.96        70
In [6]모델 저장하고 다시 불러와 예측하기
import joblib
from pathlib import Path

model_path = "models/rock_scissors_paper.pkl"
Path("models").mkdir(parents=True, exist_ok=True)
joblib.dump(model, model_path)

# 저장한 모델로 평가 데이터 한 개를 예측합니다.
loaded_model = joblib.load(model_path)
prediction = loaded_model.predict(X_test[:1])[0]
label_names = {0: "Rock", 1: "Scissors", 2: "Paper"}
print("예측 결과:", label_names[prediction])
Out[6] 출력 보기
예측 결과: Rock

같은 과제·데이터셋 비교

같은 과제·데이터셋으로 학습한 모델이 생기면 여기에서 비교됩니다.