[250826화1960H] 웹앱 모델 배포 실습 (2)

윤승호·2025년 8월 26일

생성모델보다 예측모델이 고려해야할 변수가 많은 것 같다. 그래도 오늘 꽤나 삽질한 덕분에 값진 깨달음을 얻었다...!

학습시간 09:00~02:00(당일17H/누적1960H)


◆ 학습내용

웹앱 모델 배포 실습!!

어제에 이어 4번 부터 시작!


4. 성능 개선

어제 웹앱 제작까지 마치고 0~9 숫자를 그려봤는데 모델이 대부분 5로 예측하는 문제가 있었다.

뭐가 문제일까? 양자화가 잘못되어서 성능이 떨어진 걸까?

# 모델 로드
MODEL_PATH = "mnist-12.onnx"

@st.cache_resource
def load_model(model_path):
    return ort.InferenceSession(model_path)

session = load_model(MODEL_PATH)

if session:
    input_name = session.get_inputs()[0].name
    output_name = session.get_outputs()[0].name

mnist-12-qint8.onnx 파일 대신 mnist-12.onnx 파일을 로드해서 확인해 보자. 만약 양자화 이전 파일도 같은 문제가 생긴다면 양자화의 문제는 아닐 것이다.

원본 파일을 로드해도 여전히 똑같은 문제가 생긴다. 그렇다는 건 이미지 전처리 후 모델에 넘기는 과정에서 문제가 발생했다는 뜻이다.

def preprocess_image(image_data):
    # RGBA -> Grayscale
    img = Image.fromarray(image_data.astype('uint8'), 'RGBA').convert('L')
    # 28x28로 리사이즈
    img_resized = img.resize((28, 28), Image.Resampling.LANCZOS)
    # Numpy 배열로 변환하고 0~1 사이로 정규화
    img_array = np.array(img_resized, dtype=np.float32) / 255.0
    # 모델 입력 형식에 맞게 차원 확장 (1, 1, 28, 28)
    processed = np.expand_dims(img_array, axis=(0, 1))
    return processed

기존 이미지 전처리 코드다. 리사이즈, 흑백변환, 정규화, 차원변환 말고 특별한 건 없는데 어디서 문제가 된 걸까...?

애초에 이게 문제가 아닌 건 아니었을까...?

MNIST 원본 데이터셋 이미지를 들여다 봤다.

갑자기 뭔가 번뜩 뇌리를 스쳤다. 데이터셋 배경은 검은색인데, 캔버스 배경은 흰색이다...! 혹시 이게 문제인가!?!?

아무리 정규화를 한다고 한들 모델이 학습한 데이터와 차이가 있을 게 분명하다. 이 가설이 맞다면 캔버스 그림은 학습 데이터에 어긋나기에 예측을 제대로 못하는 게 어느 정도 이해가 된다.

그럼 만약 캔버스 인풋을 MNIST 학습 데이터와 동일하게 검은색 배경에 흰색 숫자로 하면 어떨까?? 제대로 예측할까??

문제를 찾았다. 아주 잘 예측한다. 결국 색상의 문제였던 것이다.

그럼 색상을 어떻게 설정하든 모델 인풋으로 들어갈 때 검정색 배경 & 흰색 숫자로 변경되도록 코드를 수정하면 해결되지 않을까?

def hex_to_rgb(hex_color):
    hex_color = hex_color.lstrip('#')
    return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))

def preprocess_image(image_data, bg_color_hex):
    # 사이드바의 배경색(hex)을 RGB 튜플로 변환
    bg_rgb = hex_to_rgb(bg_color_hex)
    
    # 원본 이미지에서 배경색과 다른 픽셀을 모두 찾아서 마스크 생성
    # image_data의 RGB 채널(앞의 3개)만 비교
    rgb_data = image_data[:, :, :3]
    is_drawing_mask = ~np.all(rgb_data == bg_rgb, axis=2)
    
    # 새까만 배경의 새 이미지 생성
    new_image = np.zeros((image_data.shape[0], image_data.shape[1]), dtype=np.uint8)
    
    # 마스크를 이용해 그려진 부분만 255(흰색)으로 칠하기
    new_image[is_drawing_mask] = 255
    
    # 리사이즈, 정규화, 차원 확장
    img_pil = Image.fromarray(new_image)
    img_resized = img_pil.resize((28, 28))
    img_array = np.array(img_resized, dtype=np.float32) / 255.0
    processed = np.expand_dims(img_array, axis=(0, 1))
    
    return processed

전처리 코드를 수정했다.

사이드바에서 선택한 색상 hex 코드를 RGB 채널로 변경 후 이 값과 다른 색상을 모두 찾는다. 즉, 숫자 영역을 찾는다.

이후 검은색 빈 영역을 생성해서 방금 찾은 숫자 영역 값을 전부 255(흰색)으로 변경 후 위에 올린다. 그럼 내가 원하던 검은색 배경 & 흰색 숫자가 된다.

이렇게 하면 모델에 들어가는 인풋은 캔버스 색상에 관계없이 무조건 검은색 배경 & 흰색 숫자가 된다.

한번 실험해 볼까?

색상을 조잡하게 바꾸면서 실험했는데도 94%~100%의 정확도를 보여준다.

아주 좋군!

# 모델 로드
MODEL_PATH = "mnist-12-qint8.onnx"

다시 양자화 qint8 파일을 로드해서 예측을 진행해 보자.

전처리가 근본적인 문제였다면 양자화 파일로도 정상적으로 작동해야 한다.

굿!! 양자화 파일로 해도 흰색 배경 & 검은색 숫자를 제대로 예측한다.


5. 배포

미션 목적이 도커 배포까지 하는 것이었으니 도커파일을 작성해 보자!

# 베이스 이미지 설정
FROM python:3.11-slim

# 작업 디렉토리 설정
WORKDIR /app

# requirements.txt 복사 및 라이브러리 설치
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# 소스 코드 복사
COPY . .

# Streamlit 포트 노출
EXPOSE 8501

# 컨테이너 실행 시 Streamlit 앱 실행
CMD ["streamlit", "run", "app.py"]

일단 Dockerfile에 들어갈 내용을 쭉 적어준다. requirements.txt로 패키지 버전을 맞춰주고, 나머지 파일은 전부 복사!

# requirements.txt

streamlit
streamlit-drawable-canvas
numpy
onnxruntime
Pillow
scipy

requirements.txt에는 간단하게 필요해 보이는 것들만 넣었다.

conda로 패키지 버전을 쭉 뽑아서 넣었더니 용량 5GB가 넘어가더라... 내 소중한 데이터를 여기에 쓸 순 없지...

# 프로젝트 이름
name: 'mnist-project'

services:
  # 서비스 이름
  app:
    # 현재 폴더의 Dockerfile 사용
    build: .
    # 포트 연결
    ports:
      - "8501:8501"
    # 재시작 프로세스
    restart: unless-stopped

매번 container run 하기 귀찮으니 docker-compose.yaml 파일도 만들어 준다.

docker compose up --build

Dockerfile, yaml 파일을 이용해 이미지를 빌드해 주고 곧바로 실행까지 해본다.

docker tag mnist-project-app yoonsnowman/mnist-project-app:1.0

도커헙에 푸쉬하기 전에 tag name을 만들어 주고,

docker push yoonsnowman/mnist-project-app:1.0

도커헙에 push!!

정상적으로 업로드됐다.

마지막으로 잘 실행되나 테스트만 해보자.

docker compose up

compose로 실행!

실행도 되고 모델도 잘 작동한다.

끝!

profile
나는 AI 엔지니어가 된다.

0개의 댓글