[머신러닝] 딥러닝 (11) - CNN GAN

julian·2025년 5월 12일

python

목록 보기
49/74
post-thumbnail

❗❗ 어려움 주의 ❗❗


1. GAN(Generative Adversarial Network)

가상의 생성형 이미지를 만들어내는 GAN이다.
자연어 처리도 챗봇이나 번역쪽으로 포커스가 맞춰지다가 생성형이 나오게 됐다.
질문을 줬을때 만들어주는 것인데, 이미지도 똑같다.

이미지의 스타일을 교체하거나 이미지의 특정 특징만 변환하는 등 다양한 역할을 한다.

GAN 모델의 종류는 정말 많은데, 그 중 DCGAN을 진행해보자.
Deep Learning의 Conv를 이용하는 GAN이라고 보면 된다.

인공지능이 인간의 지능을 모방한다는 관점에서 볼때, 인간의 지능으로는 생전 처음 보는 종의 개라고 할지라도 '개'로 분간할 수 있다.
그렇게 인간은 추상적 개념도 범주화 할 수 있다.
게임이라는 단어를 들으면 컴퓨터나 스마트폰을 떠올리지만, 보드게임, 카드게임, 학교나 동네에서 하는 놀이 등도 게임으로 인식하듯이 우리의 뇌는 수많은 개념을 특정한 기준에 따라 자유자재로 분류할 수 있다.
그리고 인간은 개를 그릴 수도 있다.

결국 머신러닝이나 딥러닝은 우리의 뇌를 형상화 한 것이기 때문에 분류도 나온 것이고, 생성적인 측면도 가능할 것이라고 해서 GAN 생성모델이 시작됐다.

1.1. GAN의 전체적인 개념

이 GAN의 전체적인 개념은 위조 작가와 감별사가 있다고 보면 된다.
위조 작가가 처음에는 그림을 못그리는데 점점 학습할 수록 잘 그리게 된다.
그리고 감별사도 처음에는 가짜인지 구분을 못하다가 학습할 수록 잘 구분하게 된다.
이렇게 Generator(생성자)discriminator(감별자)
그래서 서로 경쟁하며 발전하는 것이 GAN 모델의 기본 개념이다.

생성자는 처음에 X값이 없다.
랜덤한 임의의 값을 만들어서 가짜 그림을 감별자에게 넘기게 된다.
그러면 감별자는 X값으로 진짜 이미지와 가짜 이미지를 학습하게 된다.
그렇게 감별자는 이제 진짜는 1로 가짜는 0으로 분류해야한다.

  • 순전파 -> 역전파
    진짜 이미지를 0.6정도로 분류했다면 1과의 차이가 있어 역전파,
    가짜 이미지를 0.7로 분류했다면 1과의 차이가 있어 역전파를 진행하는 것이다.

  • 판별자의 입장
    실제 이미지가 들어올 경우에는 1이 되어야 하며,
    가짜 이미지가 들어올 경우에는 0이 되어야 한다.

  • 생성자 입장
    판별자가 가짜를 진짜로 인식하도록 1이 되도록 해야한다.

2. DCGAN (Deep Convolutinal Generative Adversarial Networks)

중간중간 DCGAN 논문(Radford et al., 2015)에서 권장된 내용들을 사용한다.

2.1. 생성자 모델 (Generator Model)

g_model=keras.Sequential()

# 생성자는 지금 내부에 X가 없기 때문에 랜덤한 100차원 노이즈(z)를 입력으로 받도록 만들어줘야함
g_model.add(keras.layers.Input(shape=(100,)))  

# 100차원 노이즈를 128채널 7*7 feature map으로 만들기 위한 1차원 벡터를 생성
g_model.add(keras.layers.Dense(128*7*7))  

# 이제 activation function을 걸어줘야하는데,
# relu는 0보다 작으면 0, 양수는 무한대
# 그런데 0보다 작을 경우에 없애면 이미지 손실이니까 0.2를 곱하는 LeakyReLU를 사용
g_model.add(keras.layers.LeakyReLU(0.2))

# 출력값을 정규화하여 학습을 안정시키고 속도를 높임 (가중치가 아니라 출력임)
# 평균 0, 분산 1
g_model.add(keras.layers.BatchNormalization())

# CNN 레이어가 받아들일 수 있는 형태로 변환
# 1차원 벡터(6272)를 7x7 크기의 128채널 이미지로 재구성
# 점점 키워가는 거임
g_model.add(keras.layers.Reshape((7,7,128)))
g_model.add(keras.layers.UpSampling2D())  # 두 배로 증가 14 14 128

# 선형으로하고
g_model.add(keras.layers.Conv2D(64, kernel_size=5, padding='same'))  # 14 14 64
# 다듬어주고
g_model.add(keras.layers.BatchNormalization())
# 비선형으로
g_model.add(keras.layers.LeakyReLU(0.2))

g_model.add(keras.layers.UpSampling2D())  # 28 28 64

# 원본이 28*28*1이기 때문에 맞춰주고
# tanh를 쓰는이유는
# sigmoid는 0~1 사이의 값을 내고, tanh는 -1~1 사이 값을 내기 때문에 이미지 생성에서 중심이 0인 정규화에 더 유리하기에 tanh 사용
g_model.add(keras.layers.Conv2D(1, kernel_size=5, padding='same', activation='tanh'))  

초반에 들어온 노이즈 백터는 완전 랜덤한 값이니까 먼저 비선형셩(LeakyReLU)을 줘서 약간의 특성을 부여하고
그걸 정규화해서 분포를 안정화시키는 구조로 진행했고,
즉 실험적으로 더 나은 Generator 초기 결과를 만들기도 해서 처음에는 LeakyReLU -> 정규화 순서로 갔고,

이후 다음부터는 가장 많이 쓰이는 표준 패턴인 정규화 이후 비선형으로 갔다.

좀 더 선형 -> 비선형 흐름을 보자면,
Conv2D는 선형 연산을 수행해서, 입력 이미지에 커널(필터)을 씌워서 가중치와 곱해서 더한 결과를 출력한다.
즉, 수학적으로 보면 행렬곱 + 편향이라는 선형 계산만 해준다.
하지만 이대로만 하면 모델은 선형 함수밖에 학습 못해서 복잡한 패턴을 학습 못하게 된다.

그 이후에 학습을 빠르고 안정적으로 만들기 위해 배치정규화로 선형 연산 결과를 평균 0, 분산 1로 정규화해줬다.

그 다음으로 이제 LeakyReLU를 진행했는데, 이게 비선형 함수다.
비선형 활성화 함수는 입력값에 따라 출력을 비선형적으로 바꾸기 때문에, 모델이 복잡한 패턴을 학습할 수 있다.

즉 이 순서를 사용하는 이유는
Conv2D: 기본 계산(특성 추출) -> BatchNorm: 계산 결과 정리 -> LeakyReLU: 비선형성 부여하여 모델이 복잡한 함수도 학습 가능

2.2. 판별자 모델 (Discriminator Model)

d_model=keras.Sequential()

# 이 판별자 모델은 들어오는 X값이 있기 때문에(숫자 흑백 MNIST 입력으로 직접 받음)
# 생성자는 z라는 노이즈만 입력받았지만, 판별자는 실제 이미지(X)가 들어옴
d_model.add(keras.layers.Input(shape=(28,28,1)))

# 이전 Autoencoder에서 설명했듯이  
# MaxPooling은 큰 값만 뽑아내기 때문에 정보를 너무 과감하게 버릴 수 있기에
# strides를 사용하면 다운 샘플링과 특정 추출을 한번에 처리 가능해서, MaxPooling의 단점을 보완할 수 있다.  
d_model.add(keras.layers.Conv2D(64, kernel_size=5, strides=2, padding='same'))  # 7 7 128
d_model.add(keras.layers.LeakyReLU(0.2))
d_model.add(keras.layers.Dropout(0.3))  

d_model.add(keras.layers.Conv2D(128, kernel_size=5, strides=2, padding='same'))
d_model.add(keras.layers.LeakyReLU(0.2))
d_model.add(keras.layers.Dropout(0.3))  

d_model.add(keras.layers.Flatten())  # 7*7*128=6272

# 생성자에서는 생성된 이미지의 픽셀 값을 -1~1 범위로 만들기 위해 출력층 activation을 tanh를 사용했지만,  
# 판별자에서는 출력값이 0(가짜) 또는 1(진짜) 인 확률값이어야 하기 때문에 sigmoid를 사용한다.(-> 그 이유로 binary_crossentropy)
d_model.add(keras.layers.Dense(1, activation='sigmoid'))

# learning_rate제외하고 하나 더있는데,  
# beta_1=0.5: 이는 momentum 계수인데, 이전 기울기의 방향을 얼마나 보존할지 결정하는 하이퍼파라미터다.  
# 기본값은 0.9인데 DCGAN에서는 불안정한 훈련을 안정화하기 위해 0.5로 낮춘다고 논문에서 제안된 하이퍼파라미터다.  
# 즉 beta_1=0.5는 gradient 방향을 50&만 유지하면서 덜 관성적으로 가중치를 업데이트하겠다는 의미다.  
d_model.compile(optimizer=Adam(0.002, 0.5), loss='binary_crossentropy', metrics=['accuracy'])  

⭐ DCGAN의 학습 흐름

판별자 입장에서 학습때 두개가 넘어온다 -> 진짜와 가짜.
가짜가 넘어올 때를 보면, 가짜가 넘어오면 계산을 할 것인데,
계산을 해서 0.7이 나왔다 하자. 그러면 이는 0에 가까워야하는데,
어쨌든 계산을 한다는 것은 가중치 값이 나온다는 것인데, 그때 가중치 3개가 나왔다 하자.

real도 계산해서 0.6이 나와서 1에 가까워져야하고, 가중치도 3개가 나왔다고 하자.

그러면 loss도 구하고 역전파해서 각각 최종 가중치 값을 가지고 있을 것이다.
그리고 나서 이를 generator로 넘겨주는 것이다.
중요한 건 생성자는 X가 없고 판별자는 X가 있다는 것이다.
그러면 생성자가 그 가중치를 가지고 만들어 낸다.

이를 조금 더 정리하면 다음과 같다.

  1. Discriminator의 입장에서 보면
    학습 때 두 가지 입력을 받음
    진짜 이미지 (Real) → 레이블은 1
    생성된 가짜 이미지 (Fake) → 레이블은 0
    각각을 판별해서 확률을 출력 (예: 0.7, 0.6 등)
    진짜 이미지 → 1에 가까워야 하고
    가짜 이미지 → 0에 가까워야 함

  2. Loss 계산과 역전파 (Discriminator만)
    Binary Crossentropy를 통해 real/fake 각각에 대해 loss를 계산
    이 loss를 바탕으로 Discriminator의 가중치를 업데이트

  3. 이후 Generator 학습
    Generator는 **"Discriminator를 속이도록" 학습함
    즉 Discriminator가 생성된 이미지를 1 (진짜)로 오해하게 만드는 것이 목표
    이때 중요한 건
    Generator에는 실제 Y값(X에 대한 정답)이 없고
    대신 Discriminator를 통해 나온 loss(판별 결과)를 기반으로 역전파만 받는다는 것.

  4. 결과적으로
    Generator는 Discriminator로부터 받은 “피드백(가중치 업데이트 방향)”을 가지고 학습함
    Discriminator는 X가 있고, Generator는 X 없이 역전파로만 학습하는 구조

  5. 정리
    Discriminator는 진짜/가짜 이미지에 대한 정답(X, y)이 있어서 직접 학습하고,
    Generator는 정답이 없기 때문에 Discriminator로부터 역전파된 오차만 가지고 학습함

2.3. GAN 모델 생성

판별자와 생성자를 만들었으니 이제 연결하는 모델이다.

단 GAN 모델이란 것은 없다.
지금까지는 만들어져있는 모델을 가져다가 썼는데, 생성자와 판별자의 모델을 가져다가 GAN이라는 모델을 만들어내야 한다.

g_input=keras.Input(shape=(100,))  # 랜덤한 100차원 노이즈(z)
# 노이즈 -> g_model 이미지 생성 -> d_model로 넘어감
d_output=d_model(g_model(g_input))  # 노이즈가 넘어갔고 -> 결과가 나왔다 -> 0 또는 1이 나옴

# 이걸 이제 gan이라는 모델을 만들어서 보내주기
gan=Model(g_input, d_output)  # 노이즈와 결과가 들어감
gan.compile(optimizer=Adam(0.002, 0.5), loss='binary_crossentropy', metrics=['accuracy'])  # 생성자만 학습

2.4. ⭐ 학습 함수

def get_train(epoch, batch_size, saving_interval):  # saving_interval: 저장을 몇번에 한번씩 할 것인지
    (X_train, _), (_, _)=keras.datasets.mnist.load_data()
    
    # activation fuction이 tanh이므로 -1~1로 정규화해야하므로 기존에는 255.0으로 나눠줬었는데 *2-1로 해줘야함
    X_train=(X_train.reshape(-1, 28, 28, 1) / 255.0)*2-1  
    
    # label을 만들어줘야함
    true=np.ones((batch_size, 1))  # REAL -> 실제 레이블을 (배치 사이즈만큼 생성, 1차원)
    fake=np.zeros((batch_size, 1))  # FAKE -> 가짜(가상) 레이블을 (배치 사이즈만큼 생성, 1차원)
    
    history=[]
    for i in range(epoch):
        # 1. REAL *************************************************************
        # 판별자 모델 -> 실제 이미지를 판별
        
        d_model.trainable=True
        
        # 인덱스값을 0부터 60000 사이의 숫자 중 랜덤하게 배치 사이즈만큼 뽑아냄
        idx=np.random.randint(0, X_train.shape[0], batch_size)  # 확인하려면 print(idx) 찍어보기
        imgs=X_train[idx]  # 실제 이미지
        
        # 이제 판별자 모델에 진짜 이미지를 보내주고 이게 true라는 것도 전달해줌  
        # 즉 이 train_on_batch가,
        # 기존의 fit(d_model.fit(X_train, Y_train))과 같은 것으로 -> X가 imgs, Y가 true가 되는 것임
        # 그래서 실제 이미지의 loss값을 뽑아냄
        
        # 단 이때, d_model.train_on_batch(imgs, true) 이는 판별자 모델만 학습시키고 생성자 레이어와는 무관하다.
        d_loss_real=d_model.train_on_batch(imgs, true)  
        
        
        # 2. FAKE *************************************************************
        # 판별자 모델 -> 가짜(가상) 이미지를 판별
        # 실수 0~1 사이의 숫자 중 랜덤하게 배치 사이즈만큼 반복하는데 100열(차원) 뽑아라
        noise=np.random.normal(0, 1, (batch_size, 100))  
        
        # 예측에 noise를 넣으면 값을 반환해주니까 그걸 저장한 가짜(가상) 이미지가 g_imgs
        g_imgs=g_model.predict(noise, verbose=0)
        
        
        # 가짜(가상) 이미지 g_imgs와 fake라는 것을 보내줌
        # 그래서 가짜(가상) 이미지의 loss값을 뽑아냄
        d_loss_fake=d_model.train_on_batch(g_imgs, fake)
        
        
        # ------------------ 생성자 --------------------
        # 판별할때는 학습기능을 켜주고 True
        # 생성할때는 학습기능을 꺼줘야함 False
        d_model.trainable=False
        
        # 판별자의 오차
        # d_loss = (d_loss_real+d_loss_fake)/2 인데, 보통 속도향상을 위해 *0.5를 사용  
        d_loss=0.5*np.add(d_loss_real, d_loss_fake)  
        
        
        # 생성자는 이제 gan에서 불러주는데,  
        # noise 이미지를 보내주면서 "이거 다 진짜야"라고 학습을 시켜야함
        
        # 이 부분은 판별자가 건낸 모델가중치만 동결시키고
        # 생성자가 생성한 이미지를 판별자에게 true 레이블로 줘서 학습 -> 생성자 가중치만 학습
        gan_loss=gan.train_on_batch(noise, true)
        
        if i % saving_interval == 0:  # 넘어온 애 배수만큼 
            print('epoch:%d' % i, ' d_loss:%.4f' % d_loss[0], 'd_accuracy:%.4f' % d_loss[1], 
                ' gan_loss:%.4f' % gan_loss[0], 'gen_accuracy:%.4f' % gan_loss[1])
            
            noise=np.random.normal(0,1,(25,100))  # 0~1사이 실수 25번 만드는데, 100열 만듬
            g_imgs=g_model.predict(noise, verbose=0)
            # 정규화했기 때문에 0.5씩 곱해줌
            g_imgs=0.5*g_imgs*0.5
            
            fig, axs=plt.subplots(5,5)
            count=0
            for j in range(5):
                for k in range(5):
                    axs[j,k].imshow(g_imgs[count, :, :, 0], cmap='gray')  # count, 행, 열, 0
                    axs[j,k].axis('off')
                    count += 1
            fig.savefig("deep_result/gan_mnist/gan_mnist_%d.png" % i)
            plt.close(fig)
get_train(10001, 32, 200)
epoch:0  d_loss:0.7267 d_accuracy:0.4453  gan_loss:0.3498 gen_accuracy:1.0000
epoch:200  d_loss:0.6736 d_accuracy:0.5739  gan_loss:1.0875 gen_accuracy:0.1690
epoch:400  d_loss:0.6787 d_accuracy:0.5819  gan_loss:1.1744 gen_accuracy:0.1354
epoch:600  d_loss:0.6820 d_accuracy:0.5795  gan_loss:1.1452 gen_accuracy:0.1319
...
...
epoch:9600  d_loss:0.6976 d_accuracy:0.5921  gan_loss:1.2862 gen_accuracy:0.1537
epoch:9800  d_loss:0.6979 d_accuracy:0.5919  gan_loss:1.2859 gen_accuracy:0.1542
epoch:10000  d_loss:0.6982 d_accuracy:0.5918  gan_loss:1.2856 gen_accuracy:0.1545

0일때

10000일때


⭐ 최종 정리

GAN의 학습 흐름 요약

GAN은 두 가지를 번갈아가며 학습한다.

1단계: 판별자(D) 학습

  • 진짜 이미지 vs 생성된 이미지(가짜)를 구분하도록 판별자를 학습
  • 이때는 판별자만 학습, 생성자는 학습하지 않음
d_model.trainable = True  # 판별자 ON
gan.trainable = False     # 생성자는 GAN 내부이지만 이 단계에서는 학습 안함

2단계: 생성자(G) 학습

  • 생성자가 만든 이미지가 판별자 눈에 진짜처럼 보이도록 생성자만 학습
  • 판별자의 가중치는 고정되어 있어야 함 (속이면 안됨!)
d_model.trainable = False  # 판별자 OFF 해야 함 (판별자 가중치 고정!)
gan.trainable = True       # 생성자는 GAN 모델 통해 학습

전체로 보면,
1. 생성자에서 노이즈를 생성해서 판별자로 넘겨준다.
2. 판별자에서 실제 이미지와 가짜 이미지를 판단한다.
3. 오차가 나온다.
4. 역전파를 통해 판별자의 가중치가 업데이트된다. -> "판별자 학습 단계"일 때만!
5. 생성자는 업데이트된 판별자를 기준으로 진짜처럼 속이려 한다.
6. 그때는 판별자는 고정되고, 생성자만 학습된다.

판별자 학습 단계: 진짜/가짜 잘 구분하도록 -> d_model.trainable=True -> gan 모델 사용 안함 -> 학습 대상은 판별자
생성자 학습 단계: 진짜처럼 보이도록 속이기 -> d_model.trainable=False -> gan 모델 사용 -> 학습 대상은 생성자

즉,d_model.trainable = False는 "생성자만 학습할 때" 사용하며
판별자의 역할은 "생성자의 평가자"여야지, "코치"가 되면 안됨

그래서 생성자 학습 시에는 판별자 고정
gan.compile(...)을 할 땐, 항상 그 직전에 d_model.trainable = False를 둬야 함.
왜냐면 compile()할 때 trainable 플래그를 기반으로 모델이 최적화 대상 파라미터를 설정하기 때문.

profile
AI Model Developer

0개의 댓글