from keras.datasets import mnist
(tr_x,tr_y),(tt_x,tt_y)=mnist.load_data()
👉 MNIST 데이터 로드
tr_x, tr_y: 훈련 데이터(이미지, 레이블)
tt_x, tt_y: 테스트 데이터(이미지, 레이블)
s_tr_x=tr_x.reshape(-1,28,28,1)/255.
s_tt_x=tt_x.reshape(-1,28,28,1)/255.
👉 이미지 정규화 및 형상 변경
원래 MNIST 데이터는 (60000, 28, 28) 형식이지만 CNN을 쓰려면 (배치, 높이, 너비, 채널) 형태가 필요!
reshape(-1,28,28,1) → 마지막 1은 흑백 이미지(채널 1개)라는 의미.
/255. → 픽셀값(0255)을 01로 정규화해서 학습 효율을 높임.
from keras.utils import to_categorical
s_tr_y=to_categorical(tr_y)
s_tt_y=to_categorical(tt_y)
👉 원-핫 인코딩 (One-hot Encoding)
MNIST의 숫자 라벨(0~9)을 원-핫 벡터로 변환해줌.
예) 3 → [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
class CNN:
def __init__(self,n_k=10,batch_size=32,units=10,lr=0.1):
👉 CNN 모델을 만들기 위한 초기 설정
n_k=10 → 합성곱 커널(필터) 개수
batch_size=32 → 한 번에 학습할 데이터 개수
units=10 → 은닉층 뉴런 개수
lr=0.1 → 학습률
👉 CNN의 기본 구조를 따른 순전파 과정
def forpass(self,x):
c_out=conv2d(x,self.c_w,strides=1,padding='SAME') # 합성곱 연산
r_out=relu(c_out) # 활성화 함수 (ReLU)
p_out=max_pool2d(r_out,ksize=2,strides=2,padding='VALID') # 풀링
f_out=tf.reshape(p_out,[x.shape[0],-1]) # 벡터화
z1=tf.matmul(f_out,self.w1)+self.b1 # 은닉층 연산
a1=relu(z1) # 은닉층 활성화
z2=tf.matmul(a1,self.w2)+self.b2 # 출력층 연산
return z2
합성곱(Convolution) → 이미지 특징 추출
ReLU 활성화 함수 → 비선형성 추가
맥스 풀링(Max Pooling) → 특징 유지하면서 크기 축소
벡터화 (Flatten) → CNN 출력을 완전연결층으로 변환
은닉층(Dense Layer) → 뉴런을 거쳐 활성화
출력층(Dense Layer) → 최종 출력 계산
def init_w(self,input_shape,n_class):
g=tf.initializers.glorot_uniform(0) # Glorot 초기화
self.c_w=tf.Variable(g((3,3,1,self.n_k))) # 합성곱 가중치
self.c_b=tf.Variable(np.zeros(self.n_k),dtype=float) # 합성곱 절편
n_f=input_shape[1]//2*input_shape[2]//2*self.n_k # 벡터화 후 뉴런 수
self.w1=tf.Variable(g((n_f,self.units))) # 은닉층 가중치
self.b1=tf.Variable(np.zeros(self.units),dtype=float) # 은닉층 절편
self.w2=tf.Variable(g((self.units,n_class))) # 출력층 가중치
self.b2=tf.Variable(np.zeros(n_class),dtype=float) # 출력층 절편
👉 모델의 가중치(Weight)와 절편(Bias) 초기화
Glorot 초기화 → 적절한 분포로 가중치를 생성하여 학습 안정화
합성곱, 은닉층, 출력층의 가중치와 절편을 설정
def fit(self,x,y,epoch=100,val_x=None,val_y=None):
self.init_w(x.shape,y.shape[1]) # 가중치 초기화
self.optimizer = tf.optimizers.SGD(learning_rate=self.lr) # SGD 옵티마이저
for i in range(epoch):
batch_loss=[]
for x_b , y_b in self.gen_barch(x,y):
self.training(x_b,y_b) # 배치별 학습
batch_loss.append(self.get_loss(x_b,y_b)) # 손실값 저장
tr_loss=np.mean(batch_loss) # 평균 훈련 손실
val_loss=self.get_loss(val_x,val_y) # 검증 손실
print(f"에포크:{i+1}:tr_loss:{tr_loss},val_loss:{val_loss}")
self.losses.append(tr_loss) # 훈련 손실 저장
self.val_losses.append(val_loss) # 검증 손실 저장
👉 모델 학습 과정
init_w()를 호출해 가중치 초기화
SGD 옵티마이저 설정
배치 단위로 데이터를 학습시키며 손실 계산
손실 값(losses, val_losses)을 저장하고 출력
def gen_barch(self,x,y):
bins=len(x)//self.batch_size
idx=np.random.permutation(np.arange(len(x))) # 데이터 셔플링
x=x[idx]
y=y[idx]
for i in range(bins):
st=self.batch_size*i
end=self.batch_size*(i+1)
yield x[st:end],y[st:end]
👉 배치 데이터 생성
랜덤하게 섞어서 미니배치를 만든 후, 모델에 입력
def training(self,x,y):
with tf.GradientTape() as taps:
z=self.forpass(x) # 순전파
loss=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(y,z)) # 손실 계산
w_l=[self.c_w,self.c_b,self.w1,self.b1,self.w2,self.b2]
g=taps.gradient(loss,w_l) # 가중치에 대한 그래디언트 계산
self.optimizer.apply_gradients(zip(g,w_l)) # 옵티마이저 적용
👉 경사 하강법 적용
GradientTape()를 이용해 미분(기울기) 계산
가중치를 업데이트하여 학습 진행
m=CNN(n_k=10,batch_size=128,units=100,lr=0.01)
m.fit(s_tr_x,s_tr_y,10,s_tt_x,s_tt_y) # 학습 실행
👉 CNN 모델 생성 후 학습 진행 (에포크 10번)
m.score(s_tr_x,s_tr_y) # 훈련 데이터 정확도
m.score(s_tt_x,s_tt_y) # 테스트 데이터 정확도
👉 정확도를 계산하여 모델의 성능 평가
plt.plot(m.losses[-3:])
plt.plot(m.val_losses[-3:])
👉 손실 그래프를 그려 학습이 잘 진행됐는지 확인

📌 아래부터는 은닉층 개수를 다르게 설정한 모델들을 비교하며 성능 차이 분석해보기 (DNN)
from keras import Sequential, Input
from keras.layers import Dense, Dropout, Flatten, BatchNormalization
👉 필요한 라이브러리 불러오기
Sequential : 레이어를 순차적으로 쌓는 모델
Input : 입력 크기 정의
Dense : 완전연결(Dense) 레이어
Flatten : 2D 데이터를 1D 벡터로 변환 (신경망에 넣기 위해 필요)
(tr_x, tr_y), (tt_x, tt_y) = mnist.load_data()
s_tr_x = tr_x.reshape(-1, 28, 28, 1) / 255.
s_tt_x = tt_x.reshape(-1, 28, 28, 1) / 255.
s_tr_y = to_categorical(tr_y)
s_tt_y = to_categorical(tt_y)
👉 MNIST 데이터 전처리
reshape(-1, 28, 28, 1): CNN에서는 채널을 추가해야 하지만, 여기선 DNN이므로 사실상 필요 없음.
Flatten() 층이 있기 때문에 (28,28,1) 대신 (28,28)로 입력 가능.
to_categorical() : 숫자(0~9)를 원-핫 인코딩 변환.
m = Sequential()
m.add(Input(shape=(28, 28))) # 입력층
m.add(Flatten()) # 2D 데이터를 1D로 변환
m.add(Dense(100, activation='relu')) # 은닉층 (100개의 뉴런, 활성화 함수 ReLU)
m.add(Dense(10, activation='softmax')) # 출력층 (10개의 클래스)
m.compile(loss='categorical_crossentropy', optimizer='sgd')
hy = m.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y))
👉 DNN 모델 1
은닉층 1개 (100개 뉴런)
ReLU 활성화 함수 사용
softmax를 사용해 다중 분류 수행
SGD(확률적 경사 하강법) 최적화
categorical_crossentropy 손실 함수 적용
m1 = Sequential()
m1.add(Input(shape=(28, 28)))
m1.add(Flatten())
m1.add(Dense(100, activation='relu'))
m1.add(Dense(100, activation='relu'))
m1.add(Dense(10, activation='softmax'))
m1.compile(loss='categorical_crossentropy', optimizer='sgd')
👉 DNN 모델 2
은닉층 2개 (100개 뉴런씩)
더 깊은 네트워크로 학습이 더 잘될 가능성이 있음.
m2 = Sequential()
m2.add(Input(shape=(28, 28)))
m2.add(Flatten())
m2.add(Dense(100, activation='relu'))
m2.add(Dense(100, activation='relu'))
m2.add(Dense(100, activation='relu'))
m2.add(Dense(10, activation='softmax'))
m2.compile(loss='categorical_crossentropy', optimizer='sgd')
👉 DNN 모델 3
은닉층 3개 (100개 뉴런씩)
네트워크가 더 깊어지므로 더 복잡한 패턴을 학습할 가능성이 높음.
m3 = Sequential()
m3.add(Input(shape=(28, 28)))
m3.add(Flatten())
m3.add(Dense(100, activation='relu'))
m3.add(Dense(100, activation='relu'))
m3.add(Dense(100, activation='relu'))
m3.add(Dense(100, activation='relu'))
m3.add(Dense(10, activation='softmax'))
m3.compile(loss='categorical_crossentropy', optimizer='sgd')
👉 DNN 모델 4
은닉층 4개 (100개 뉴런씩)
모델이 깊어질수록 학습이 잘 될 수도 있지만, 과적합(overfitting)의 위험도 있음.
hy1 = m1.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y), verbose=2)
hy2 = m2.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y), verbose=2)
hy3 = m3.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y), verbose=2)
👉 각 모델 학습 실행
epochs=10 → 10번 반복 학습
batch_size=128 → 한 번에 128개 샘플을 학습
validation_data=(s_tt_x, s_tt_y) → 테스트 데이터로 검증
verbose=2 → 학습 로그를 깔끔하게 출력
plt.plot(hy1.history['loss'])
plt.plot(hy1.history['val_loss'])
👉 훈련 손실과 검증 손실 비교
훈련 손실이 계속 감소하면서 과적합 여부 확인
검증 손실이 증가하면 모델이 학습 데이터를 과하게 외우고 있는 것 (즉, 일반화 성능이 떨어짐)

파란색 선: 훈련 손실 값 (hy1.history['loss'])
주황색 선: 검증 손실 값 (hy1.history['val_loss'])
plt.plot(hy2.history['loss'])
plt.plot(hy2.history['val_loss'])

plt.plot(hy3.history['loss'])
plt.plot(hy3.history['val_loss'])

초기 손실 값이 높음
빠른 손실 감소
손실 값의 수렴
검증 손실과 훈련 손실의 차이
m1.evaluate(s_tr_x, s_tr_y), m1.evaluate(s_tt_x, s_tt_y)
m2.evaluate(s_tr_x, s_tr_y), m2.evaluate(s_tt_x, s_tt_y)
m3.evaluate(s_tr_x, s_tr_y), m3.evaluate(s_tt_x, s_tt_y)

👉 각 모델의 훈련 데이터 & 테스트 데이터 성능 평가
evaluate() → 손실 값(loss)과 정확도(accuracy)를 반환
py1 = m1.predict(s_tt_x) > 0.5
py2 = m2.predict(s_tt_x) > 0.5
py3 = m3.predict(s_tt_x) > 0.5
👉 모델이 테스트 데이터에서 예측한 결과
predict() → 모델이 클래스 확률 예측
0.5 → 0.5보다 크면 해당 클래스로 판별
은닉층 개수를 다르게 한 모델을 비교하여 MNIST 분류 성능 평가
층이 많아질수록 학습 성능이 어떻게 변하는지 실험

일반적으로 은닉층이 깊어질수록 성능이 향상되지만,
너무 깊어지면 과적합이 발생할 수도 있음
손실 그래프를 통해 훈련 손실과 검증 손실의 차이를 확인하면서 과적합 여부를 판단해야 함.
👉 딥러닝에서 층을 추가하면 무조건 성능이 좋아지는 것이 아니라, 적절한 균형이 필요하다는 점을 확인하는 테스트! 🚀
📌 신경망의 깊이에 따른 성능 비교와 배치 정규화 (Batch Normalization)의 효과를 실험하는 코드
from sklearn.metrics import classification_report
print(classification_report(s_tt_y, py1))
print(classification_report(s_tt_y, py2))
print(classification_report(s_tt_y, py3))
👉 각 모델(m1, m2, m3)의 성능을 평가
classification_report()는 정확도, 정밀도(Precision), 재현율(Recall), F1-score를 출력하는 함수.
s_tt_y(실제 값)과 py1, py2, py3(예측 값)를 비교해서 성능을 확인.
ck_m = Sequential()
ck_m.add(Input(shape=(28,28)))
ck_m.add(Flatten())
for _ in range(10):
ck_m.add(Dense(100, activation='sigmoid'))
ck_m.add(Dense(10, activation='softmax'))
ck_m.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['acc'])
👉 은닉층 10개, 출력층 포함 총 12개 층을 가진 신경망
모든 은닉층은 sigmoid 활성화 함수 사용.
출력층은 softmax 사용 (10개의 숫자 분류)
hy = ck_m.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y))

👉 10번의 에포크 동안 학습
batch_size=128 → 한 번에 128개 데이터 학습
validation_data → 검증 데이터로 성능 체크
plt.plot(hy.history['loss'])
plt.plot(hy.history['val_loss'])

👉 손실값(훈련/검증) 그래프 그리기
과적합(overfitting) 여부 확인 가능 (검증 손실이 증가하면 과적합일 가능성이 높음).
초기 손실 값
손실 값의 변동
손실 값의 수렴
검증 손실의 진동
- 검증 손실이 진동하는 것은 모델이 학습 중 약간 불안정하다는 신호.
- 배치 정규화(Batch Normalization)나 적절한 하이퍼파라미터 튜닝(학습률 조정)이 필요할 수 있음.
ck_m1 = Sequential()
ck_m1.add(Input(shape=(28,28)))
ck_m1.add(Flatten())
for _ in range(10):
ck_m1.add(Dense(100, activation='sigmoid'))
ck_m1.add(BatchNormalization()) # 배치 정규화 추가
ck_m1.add(Dense(10, activation='softmax'))
ck_m1.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['acc'])
👉 배치 정규화를 적용한 신경망
hy = ck_m1.fit(s_tr_x, s_tr_y, epochs=10, batch_size=128, validation_data=(s_tt_x, s_tt_y))
👉 배치 정규화 모델 학습
plt.plot(hy.history['loss'])
plt.plot(hy.history['val_loss'])

초기 손실 값
손실 감소
검증 손실과 훈련 손실의 수렴
plt.plot(hy.history['acc'])
plt.plot(hy.history['val_acc'])

초기 정확도
정확도 수렴
과적합 없음
👉 손실 & 정확도 그래프 비교
배치 정규화를 적용한 모델이 학습 속도가 더 빠르고, 검증 성능이 더 좋아질 가능성이 높음.