이진 분류 DNN, 출력층과 손실함수 짝 맞추기
"코드는 따라 쳤는데, 왜 여기에 sigmoid를 쓰는지는 모르겠어요."
딥러닝 파트에 들어오면 이 상태가 되는 경우가 많다. Dense(1, activation='sigmoid'), loss='binary_crossentropy' 같은 코드를 외워서 쓰고 있지만, 문제가 조금만 바뀌면 손이 멈춘다. 다중 분류로 바뀌면? 회귀로 바뀌면? y가 원핫으로 되어 있으면?
문제는 이걸 세 가지 따로따로 외우려 한다는 점이다. 출력층 뉴런 개수, 활성화 함수, 손실 함수를 각각 암기 항목으로 두면 조합이 아홉 가지가 넘는다. 헷갈리는 게 당연하다.
그런데 실제로는 외울 게 하나뿐이다. 문제 유형이 정해지면 나머지 셋은 자동으로 따라온다. 그 연결 고리를 이해하면 조합표를 외울 필요가 없어진다. 이 글에서 그 고리를 만든다.
1. 출력층은 '정답의 모양'을 그대로 닮아야 한다
신경망의 마지막 층은 예측값을 내보내는 곳이다. 그래서 출력층의 모양은 우리가 원하는 정답의 모양과 같아야 한다. 이 문장 하나가 이 글의 전부다.
비유를 하나 쓰자. 택배를 보낼 때 상자를 고르는 상황과 같다. 책 한 권을 보내면 작은 상자, 냉장고를 보내면 큰 상자다. 내용물이 상자 크기를 정하지, 상자를 먼저 고르고 내용물을 맞추지 않는다. 출력층도 마찬가지다. 예측하려는 값이 어떤 형태인지가 출력층의 뉴런 개수와 활성화 함수를 정한다.
정확한 용어로 다시 정리하면 이렇다. 이진 분류의 정답은 0 또는 1이라는 하나의 값이다. 그러니 뉴런은 1개면 된다. 다중 분류의 정답은 여러 클래스 중 하나인데, 모델은 각 클래스일 확률을 모두 내놓아야 하므로 클래스 개수만큼의 뉴런이 필요하다. 회귀의 정답은 하나의 실숫값이므로 뉴런은 1개다.
여기서 이진 분류와 회귀가 둘 다 뉴런 1개라는 점을 눈여겨보자. 둘을 가르는 건 뉴런 개수가 아니라 활성화 함수다.
표를 통째로 외우려 하지 말고 첫 열에서 시작해 오른쪽으로 읽는 연습을 하자. '이진 분류다, 정답이 0 아니면 1이다, 그러니 뉴런 1개, 0과 1 사이 값을 내려면 sigmoid, 그 짝은 binary_crossentropy.' 이 흐름으로 도달하는 게 목표다.
시험에 자주 나오는 함정
이진 분류 문제이므로 출력층의 뉴런 수는 클래스 개수인 2로 설정한다.
틀렸다. 이 선지가 매력적인 이유는 논리가 그럴듯하기 때문이다. 클래스가 두 개니까 뉴런도 두 개일 것 같다. 그런데 이진 분류에서는 양성 클래스일 확률 하나만 알면 나머지는 자동으로 정해진다. 양성일 확률이 0.87이면 음성일 확률은 0.13이다. 굳이 두 개를 계산할 이유가 없다.
뉴런 2개에 softmax를 쓰는 방식이 완전히 불가능한 건 아니다. 그렇게 구성해도 학습은 된다. 다만 그때는 손실 함수도 categorical_crossentropy 계열로 바꾸고 y도 원핫으로 만들어야 한다. AICE ASSOCIATE에서 요구하는 표준 형태는 뉴런 1개에 sigmoid다.
2. sigmoid가 하는 일, softmax가 하는 일
활성화 함수를 '비선형성을 주는 장치'라고만 배우면 출력층에서 왜 이걸 쓰는지 감이 안 온다. 출력층의 활성화 함수는 역할이 다르다. 은닉층에서는 표현력을 키우는 게 목적이고, 출력층에서는 값의 범위를 정답의 범위에 맞추는 게 목적이다.
sigmoid는 어떤 실숫값이 들어와도 0과 1 사이로 눌러 준다. 아주 큰 음수가 들어오면 0에 가까워지고, 아주 큰 양수가 들어오면 1에 가까워진다. 실제로 확인해 보면 이렇다.
입력이 0일 때 정확히 0.5가 나온다는 점도 기억해 두자. 그래서 예측 클래스를 정할 때 0.5를 기준으로 삼는 게 자연스럽다.
softmax는 여러 값을 한꺼번에 받아 전체 합이 1이 되는 확률 분포로 바꾼다.
여기가 sigmoid와 결정적으로 다른 지점이다. sigmoid는 각 뉴런을 독립적으로 0과 1 사이로 누른다. 여러 뉴런에 sigmoid를 걸면 각각 0.9, 0.8, 0.7이 나올 수 있고 합이 1을 넘는다. softmax는 뉴런들끼리 경쟁시켜 합을 1로 만든다. '이 셋 중 하나'라는 다중 분류의 성격과 정확히 맞아떨어진다.
이제 잘못 골랐을 때 무슨 일이 생기는지 보자.
가장 흔한 실수는 은닉층에 쓰던 relu를 출력층에도 그대로 두는 것이다. relu는 음수를 0으로 만들고 양수는 그대로 통과시킨다. 출력 범위가 0 이상 무제한이라 1을 넘는 값이 나온다. 확률로 읽을 수 없다.
활성화 함수를 아예 빼는 것도 문제다. 같은 데이터로 실제 학습시켜 출력값을 뽑아 보면 차이가 분명하다.
활성화를 생략하면 음수가 그대로 나온다. 확률이 음수일 수는 없다. 다만 이건 회귀 출력층에서는 정상이다. 집값이나 수요량을 예측할 때는 값을 그대로 내보내야 하므로 활성화 함수를 지정하지 않는 게 맞다.
은닉층에 relu를 쓰는 이유도 한 번 짚고 가자. sigmoid를 은닉층에 깊게 쌓으면 역전파 과정에서 기울기가 계속 작아져 앞쪽 층까지 전달되지 않는다. 이걸 기울기 소실(vanishing gradient)이라고 한다. relu는 양수 구간에서 기울기가 1로 유지되기 때문에 이 문제가 덜하다. 그래서 은닉층은 relu, 출력층은 문제 유형에 맞는 함수라는 구도가 표준이 됐다.
시험에 자주 나오는 함정
softmax는 각 클래스의 확률을 독립적으로 계산하므로 출력값의 합이 1을 넘을 수 있다.
틀렸다. 독립적으로 계산하는 건 sigmoid다. softmax는 모든 출력을 지수 변환한 뒤 그 합으로 나누기 때문에 합이 반드시 1이 된다. 이 선지가 매력적인 이유는 '확률'이라는 단어에서 독립을 연상하기 때문이다. 하지만 softmax의 목적 자체가 상호 배타적인 클래스 중 하나를 고르는 것이라, 합이 1이어야 의미가 있다.
참고로 한 샘플이 여러 클래스에 동시에 속할 수 있는 다중 레이블 문제에서는 오히려 sigmoid를 여러 뉴런에 걸어 쓴다. 다만 AICE ASSOCIATE 범위는 아니다.
3. 코드로 조립하기, 그리고 짝이 안 맞을 때
이제 실제 코드를 보자. 이진 분류 DNN의 표준 뼈대는 이렇다.
Input(shape=(10,))의 10은 특성 개수다. 여기서 실수가 자주 난다. 원본 데이터의 컬럼 수가 아니라 인코딩과 전처리를 모두 마친 뒤의 컬럼 수를 넣어야 한다. 범주형 변수를 원핫으로 펼쳤다면 컬럼이 늘어나 있을 것이다. X_train.shape[1]을 그대로 넣는 습관이 안전하다.
쉼표를 빼먹으면 안 된다. (10)은 그냥 정수 10이고 (10,)이 원소 하나짜리 튜플이다. 파이썬 문법이지만 시험에서 코드 채우기 문제로 나오면 눈에 띄지 않는다.
모델을 만든 뒤 model.summary()로 구조를 확인하는 습관도 권한다. 위 모델의 출력은 이렇다.파라미터 개수 계산도 시험에 나온다. 공식은 입력 뉴런 수 곱하기 출력 뉴런 수, 더하기 출력 뉴런 수다. 마지막에 더하는 건 편향(bias)이다. 첫 층은 10 곱하기 32 더하기 32로 352, 둘째 층은 32 곱하기 16 더하기 16으로 528, 출력층은 16 곱하기 1 더하기 1로 17이다. 합이 897로 맞아떨어진다.
학습과 예측은 이렇게 이어진다.
predict()가 확률을 반환한다는 점이 중요하다. 정확도나 F1 점수를 계산하려면 클래스로 바꿔야 하는데, 여기서 0.5 기준 변환을 빼먹는 실수가 잦다. 확률값을 그대로 정확도 함수에 넣으면 값이 이상하게 나오거나 에러가 뜬다.
손실 함수와 y 형태가 어긋나면
다중 분류에서 가장 자주 터지는 지점이다. categorical_crossentropy는 y가 원핫 벡터일 것을 기대하고, sparse_categorical_crossentropy는 y가 정수일 것을 기대한다. 짝이 안 맞으면 학습이 시작되지 않는다.
에러 메시지가 친절한 편이다. target의 shape가 1차원인데 output은 3차원 열을 갖고 있다고 알려 준다. y를 원핫으로 바꾸면 해결된다.
정리하면 선택지는 두 개다. y를 원핫으로 바꾸고 categorical_crossentropy를 쓰거나, y를 정수 그대로 두고 sparse_categorical_crossentropy를 쓰거나. 후자가 변환 단계가 하나 줄어서 실기에서는 더 편하다. 이름에 붙은 sparse가 '희소하게 표현된', 즉 정수 하나로 압축된 형태를 뜻한다고 기억하면 헷갈리지 않는다.
시험에 자주 나오는 함정
model.predict()는 예측된 클래스 레이블을 반환하므로 정확도 계산에 바로 사용할 수 있다.
틀렸다. 케라스의 predict()는 출력층을 통과한 값을 그대로 반환한다. 이진 분류라면 sigmoid를 지난 확률값이고, 다중 분류라면 softmax를 지난 확률 분포다. 클래스 레이블이 아니다. 이진 분류는 0.5 기준으로 변환하고, 다중 분류는 np.argmax(pred, axis=1)로 가장 확률이 높은 인덱스를 뽑아야 한다.
이 선지가 매력적인 이유는 사이킷런의 습관 때문이다. 사이킷런 모델의 predict()는 실제로 클래스를 반환하고, 확률이 필요하면 predict_proba()를 따로 부른다. 케라스는 반대다. 두 라이브러리를 오가며 쓰다 보면 이 차이에서 걸린다.
시험 직전에 이것만
- 이진이면 1개에 sigmoid
- sparse는 y가 정수라는 뜻
- predict는 확률, 0.5로 잘라야 클래스
『빠르게 따는 AICE ASSOCIATE』에서는 어디를 보면 되나
이 글의 내용은 책 PART 5 AI 모델링 딥러닝에 집중돼 있다. 활성화 함수의 개념과 신경망 설계는 01장 인공신경망(164쪽)의 01절부터 03절 '인공신경망 설계'(172쪽)까지고, 학습 메커니즘인 순전파와 역전파는 02절 '인공신경망의 학습 메커니즘'(169쪽)에 있다. 은닉층에 relu를 쓰는 이유를 제대로 이해하려면 이 02절을 한 번 읽어야 한다.
실제 코드 조립은 03장 딥러닝 실전 모델링이다. 01절 '이진 분류 DNN'(193쪽), 02절 '다중 분류 DNN'(197쪽), 03절 '회귀 DNN'(200쪽)이 나란히 배치돼 있다. 이 배치가 학습에 유리하다. 세 절을 이어서 읽으면 이 글의 표가 자연스럽게 머리에 들어온다. 하나만 떼어 읽으면 비교가 안 되니 세 절은 붙여서 보길 권한다.
시험 직전 압축 정리는 PART 6 빈출 패턴 분석의 03장 AI 모델링이다. 03절 '딥러닝 모델 설계 및 학습'(247쪽)과 04절 '딥러닝 모델 성능 평가 및 저장'(250쪽)에서 실기 형태로 다시 정리된다. 이 글에서 다룬 predict() 반환값 처리 같은 건 04절 쪽에 붙어 있는 내용이다.
딥러닝 파트는 눈으로 읽어서는 절대 안 붙는다. Dense(1, activation='sigmoid')를 손으로 열 번 쳐 보는 게 설명을 열 번 읽는 것보다 낫다.
🖥️ 골든래빗 CBT에서 AICE ASSOCIATE 문제 풀어보기
📅 2026 AICE ASSOCIATE 시험 일정 확인하기
📘 『빠르게 따는 AICE ASSOCIATE』 자세히 보기
궁금한 게 생기면 『빠따 AICE ASSOCIATE』 공식 카카오톡 질문방에서 물어보세요. 저자들이 직접 답합니다.
