hello, robot

손글씨 숫자 데이터셋으로 신경망을 학습시키면 Dense 층만 쌓아도 90%대 초반이 나와요. 그런데 그림판으로 숫자를 하나 그려 넣어보면 엉뚱한 답이 돌아오는 일이 있어요. 학습 정확도가 90%가 넘는데 내가 그린 3을 8이라고 하는 거예요.

이럴 때 모델 구조부터 손보기 쉬운데, 원인은 대개 전처리 쪽에 있어요. 학습할 때 이미지에 한 처리를 추론할 때 그대로 하지 않으면 모델이 보는 숫자의 범위 자체가 달라지거든요.

데이터손글씨 숫자 28×28 그레이스케일 모델Dense 층 · 출력 10개 확률 전처리플래트닝 · 정규화 · 라벨 이진화 증상예외 없이 조용히 틀린 답

이미지를 넣기 전에 하는 세 가지

단계하는 일
플래트닝28×28 배열을 784 벡터로 폄Dense 층이 1차원 벡터만 받아요
정규화0~255 픽셀값을 255로 나눠 0~1로큰 값이 들어가면 가중합과 기울기가 커져 학습이 출렁여요
라벨 이진화3[0,0,0,1,0,0,0,0,0,0]출력이 10개 확률이라 정답도 같은 형태여야 비교돼요
trainX = trainX.reshape((trainX.shape[0], 28 * 28 * 1))

라벨 이진화는 원핫 인코딩(one-hot encoding)이라고도 해요. 숫자를 그대로 두면 “3은 1보다 3배 크다”는 잘못된 순서 정보가 손실 계산에 섞여요.

astype을 먼저 부르는 진짜 이유

정규화 코드가 astype("float32")를 먼저 부르는 걸 보면 정수 나눗셈 때문이라고 짐작하게 되는데, numpy 동작을 따져보면 조금 달라요. trainX / 255처럼 새 배열을 만드는 나눗셈에서는 원본이 uint8이어도 알아서 실수로 올려줘요.

a = np.array([[0, 128, 255]], dtype=np.uint8)
a / 255      # [[0. , 0.50196078, 1. ]]  — float64로 승격

문제가 되는 쪽은 제자리에서 바꾸려 할 때예요.

UFuncTypeError: Cannot cast ufunc 'divide' output from dtype('float64') to dtype('uint8')

trainX /= 255는 결과가 실수인데 담을 배열이 정수형이라 이 에러로 멈춰요. astype을 먼저 부르는 건 자료형을 실수로 맞춰 두는 일이고, float64 대신 float32를 고르는 건 메모리를 절반만 쓰기 때문이에요.

빠뜨려도 에러가 안 나요

추론 코드가 하는 일은 학습 때의 세 단계를 똑같이 반복하는 거예요. 하나라도 빠지거나 순서가 달라지면 결과가 어긋나요.

한 단계가 빠지면
형태는 맞으니 모델이 그대로 받아 계산해요. 학습 때 본 적 없는 범위라 틀릴 뿐이에요.
학습 전처리28×28 그레이펴기 → 784÷255 → 0~1모델이 본 범위추론에서 한 단계를 빠뜨리면28×28 그레이펴기 → 784(÷255 생략)예외도 경고도 없음형태는 맞으니 모델이 그대로 받아 계산하고 답을 내놓아요. 학습 때 본 적 없는 범위라 틀릴 뿐이에요.그래서 추론이 이상하면 모델보다 전처리를 먼저 의심하는 순서가 맞아요.

여기서 성가신 점은 빠뜨려도 예외도 경고도 안 난다는 거예요. 자신 있게 틀린 답이 나오니 발견이 늦어져요.

그래서 추론이 이상하면 모델보다 전처리를 먼저 의심하는 순서가 맞아요. 학습 코드와 추론 코드의 전처리 부분을 나란히 놓고 줄 단위로 비교하면 대개 거기서 끝나요.

검은 배경에 흰 글씨

전처리를 다 맞췄는데도 직접 그린 숫자만 틀린다면 남은 원인이 하나 더 있어요.

명암이 정확히 반대예요
전처리 목록에는 없지만 실질적으로 네 번째 단계예요.
손글씨 데이터셋배경 0 · 글씨 1그림판에서 그린 숫자배경 1 · 글씨 0모델 입장에서는숫자가 있는 자리가 0에 가깝고 배경이 1에 가까운,학습 때와 정반대인 입력이 들어온 셈이에요.image = 255 - image자료에는 없지만 커스텀 이미지를 넣으면 반드시 부딪히는,실질적인 네 번째 전처리 단계예요.

손글씨 숫자 데이터셋은 검은 배경에 흰 글씨예요. 반면 그림판에서 그린 숫자는 흰 배경에 검은 글씨고요. 한 줄로 뒤집어주면 해결돼요.

image = 255 - image

이건 자료에 적혀 있지 않은데 커스텀 이미지를 넣어보면 반드시 부딪히는 부분이에요.

학습과 추론 사이의 계약

세 단계든 네 단계든, 핵심은 학습 때 정한 입력 형식이 추론 때도 지켜져야 한다는 거예요. 데이터를 다루는 코드가 여러 파일로 나뉘면 이 약속이 조용히 어긋나기 쉬워요. 학습 스크립트만 고치고 추론 스크립트를 그대로 두는 식으로요.

형식을 갈아탈 때마다 정보가 빠져나가는 문제는 CAD에서 URDF까지, 포맷 변환 사슬에서 잃는 정보에서 로봇 모델 쪽으로 다룬 적이 있어요. 도메인은 다른데 증상은 닮았어요. 중간 단계에서 잃어버린 것이 한참 뒤에 이상한 결과로 나타나고, 그 사이에 아무도 에러를 내주지 않아요.

학습이 왜 활성화 함수의 미분에 기대는지는 활성화 함수의 미분 가능성과 시그모이드 기울기 소실에 정리했어요.