SGAN 소개
준지도 학습 은 GAN을 실용적으로 적용할 수 있는 기대되는 분야 중 하나이다. 데이터셋에 있는 모든 샘플에 레이블이 필요한 지도 학습이나 레이블을 사용하지 않는 비지도 학습과 달리 준 지도 학습은 훈련 데이터셋의 일부에만 클래스 레이블을 가지고 있다. 준지도 학습은 데이터에 감춰진 내부 구조를 사용해 일부 레이블된 데이터 포인트들을 일반화하고, 효율적인 방식으로 이전에 본 적 없는 새로운 샘플을 분류한다. 준지도 학습이 작동하려면 레이블된 데이터와 레이블이 없는 데이터가 동일한 분포에서 수집되어야 한다는 점이 중요하다.
레이블링 된 데이터가 부족한 것은 머신러닝 연구와 실용 애플리케이션에서 흔히 발생하는 문제 중 하나이다. 레이블이 없는 데이터는 많지만 데이터에 레이블을 부여하는 것은 비용이 비싸고 실용적이지 않으며 시간이 많이 드는 작업이다. SGAN은 이러한 레이블된 데이터가 적어도 지도학습 알고리즘과 유사한 정확도를 출력하는 GAN이다.
SGAN이란
SGAN(semi-supervised generative adversarial network)은 판별자가 다중 분류를 수행하는 생성적 적대 신경망이다. 두 개의 클래스(진짜와 가짜)만 구별하는 것이 아니라 N+1개의 클래스를 구별하도록 학습한다. N은 훈련 샘플에 있는 클래스 개수이고 한 개는 생성자가 만든 가짜 샘플을 위해 추가한다.
예를 들어 MNIST 데이터셋에 10개의 레이블이 있다고 해보자. 이 데이터셋에서 훈련한 SGAN 판별자는 10 + 1 = 11개의 클래스를 예측한다. 여기에는 SGAN 판별자의 출력이 10개 클래스에 대한 확률 벡터로 표현된다.(모두 더하면 1.0) 여기에 진짜인지 가짜인지 나타내는 확률이 하나 더 있다.

그림에서 보는 것처럼 다중 클래스를 구별하는 작업은 판별자 뿐만 아니라 전통적인 GAN보다 SGAN구조, 훈련 과정, 훈련 목적 함수에 복잡도를 추가한다.
구조
SGAN 생성자의 목적은 오리지널 GAN과 동일하다. 랜덤한 벡터를 받아 훈련 세트와 구분이 안되는 가짜 샘플을 만드는 것이다. 하지만 SGAN 판별자는 오리지널 GAN 구현과는 다르다. SGAN 판별자는 둘이 아니라 세 종류의 입력을 받는다. 생성자가 만든 가짜 샘플(x*), 훈련 데이터셋에서 레이블이 없는 진짜 샘플(x), 훈련 데이터셋에서 레이블이 있는 진짜 샘플 (x,y)이다. 여기에서 y는 훈련 샘플 x에 대한 레이블이다. 이진 분류가 아니라 SGAN 판별자의 목표는 입력 샘플이 진짜일 경우, 해당 클래스로 분류하고 아닐 경우 가짜로 처리한다.
| 생성자 | 판별자 | |
| 입력 | 랜덤 벡터(z) | 판별자는 세 종류의 입력을 받는다. - 훈련 데이터셋에서 레이블이 없는 진짜 샘플 (x) - 훈련 데이터셋에서 레이블이 있는 진짜 샘플 (x,y) - 생성자가 생성한 가짜 샘플 (x*) |
| 출력 | 가능한 한 진짜처럼 보이는 가짜 샘플 (x*) | 입력 샘플이 n개의 진짜 클래스 중 하나 또는 가짜 클래스에 속할 가능성을 나타내는 확률 |
| 목표 | 훈련 데이터셋에 있는 샘플과 구분이 안되는 가짜 샘플을 생성하여 판별자가 진짜로 분류하도록 속인다. | 생성자가 만든 모든 샘플은 가짜로 분류하고 진짜 샘플에 올바른 클래스 레이블을 할당하는 것을 배운다. |
훈련 과정
일반적인 GAN에서 D(x)와 D(x*)에 대한 손실을 계산하고 총 손실을 역전파하여 판별자의 훈련 파라미터를 업데이트하는 식으로 손실을 최소화하도록 판별자를 훈련했었다. 생성자는 합성한 가짜 샘플을 판별자가 진짜로 잘못 분류하도록 D(x*)에 대한 판별자의 손실을 역전파하여 분류 오류가 최대화되도록 훈련한다.
D(x)와 D(x*)에 더해 SGAN을 훈련하려면 지도 학습 훈련 샘플 D((x,y))에 대한 손실도 계산해야 한다. 이 손실들은 SGAN 판별자가 고려해야 할 이중 학습 목표에 해당한다. 진짜 샘플과 가짜 샘플을 구별하면서, 진짜 샘플을 올바른 클래스로 분류하도록 학습해야 한다. 원래 논문의 용어를 사용하면 이런 이중 목표는 두 종류의 손실에 해당한다. 바로 지도 손실과 비지도 손실이다.
훈련 목표
지금까지 본 모든 GAN 모델은 생성 모델이다. 이들의 목표는 실제처럼 보이는 샘플을 만드는 것이다. 따라서 생성자 네트워크의 작업이 주요 관심 대상이다. 판별자 네트워크의 주요 목적은 판별자가 높은 품질의 샘플을 만들도록 돕는 것이다. 훈련이 끝나면 판별자는 사용하지 않고 훈련이 끝난 생성자만 사용하여 실제와 같은 합성 데이터를 만든다.
하지만 SGAN에서는 판별자가 주요 관심 대상이다. 훈련 과정의 목표는 적은 양의 레이블만 사용하여 판별자를 완전한 지도 학습 분류기에 가까운 정확도를 내는 준지도 학습 분류기로 만드는 것이다. 생성자의 목표는 가짜 데이터를 생성하여 이 과정을 돕는 것이다. 생성자는 데이터에 있는 관련 패턴을 학습하여 판별자의 분류 정확도를 향상시킨다. 훈련이 끝난 후 생성자를 버리고 훈련된 판별자를 분류기로 사용한다.
SGAN 구현하기
이 과정에서는 MNIST 데이터셋에서 100개의 훈련 샘플만 사용해 손글씨 숫자를 분류하는 SGAN 모델을 만들어 볼 것이다. 생성 후에는 지도 학습 모델과 분류 정확도를 비교하여 성과를 확인해보도록 하겠다.
진짜 레이블을 분류하는 다중 분류 문제를 풀기 위해 판별자는 소프트맥스 함수를 사용한다. 이 함수는 지정된 클래스 개수 만큼 확률 분포를 반환한다. 어떤 레이블에 할당된 확률이 높을수록 판별자가 샘플이 해당 클래스에 속한다고 크게 확신한다. 분류 오차를 계산하려면 출력 확률과 원-핫 인코딩된 타깃 레이블 사이의 교차 엔트로피 손실을 사용한다.
진짜 대 가짜 확률을 출력하기 위해 판별자는 시그모이드 함수를 사용하고 이진 교차 엔트로피 손실을 역전파하여 모델 파라미터를 훈련한다.
이제 실습해보자.
설정
모델 실행에 필요한 모듈과 라이브러리를 먼저 임포트한다.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras import backend as K
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import (Activation, BatchNormalization, Concatenate, Dense, Dropout, Flatten, Input, Lambda, Reshape)
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import Conv2D, Conv2DTranspose
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.utils import to_categorical
입력 이미지 크기, 잡음 벡터 z의 크기, 준지도 분류를 위한 진짜 클래스 개수(판별자가 분류하도록 학습할 숫자당 한 개씩)를 다음처럼 지정한다.
img_rows = 28
img_cols = 28
channels = 1
img_shape = (img_rows, img_cols, channels) # 입력 이미지 차원
z_dim = 100 # 생성자의 입력으로 사용할 잡음 벡터의 크기
num_classes = 10 # MNIST 데이터셋의 클래스 개수
데이터셋
MNIST 데이터셋에 레이블된 훈련 이미지 중 일부만 훈련에 사용하고 나머지 샘플은 모두 레이블이 없는 것처럼 다룬다. 레이블된 데이터 배치를 만들 때 처음 num_labeled 개의 이미지를 사용하고 레이블이 없는 샘플 배치를 만들 때 나머지 (MNIST 총 데이터셋 - num_labeled 개) 이미지를 사용한다.
Dataset 오브젝트는 num_labeled 훈련 샘플을 반환하는 함수와 MNIST 데이터셋에서 레이블된 테스트 이미지 10000개를 반환하는 함수를 제공한다. 훈련이 끝난 후 테스트 세트를 사용하여 모델의 분류 능력이 본 적 없는 샘플에 얼마나 잘 적용되는지 평가해보겠다.
class Dataset:
def __init__(self, num_labeled):
self.num_labeled = num_labeled # 훈련에 사용할 레이블된 샘플 개수
(self.x_train, self.y_train), (self.x_test, self.y_test) = mnist.load_data() # MNIST 데이터셋 적재
def preprocess_imgs(x):
x = (x.astype(np.float32) - 127.5) / 127.5 # [0,255] 사이 흑백 픽셀 값을 [-1,1] 사이로 변환
x = np.expand_dims(x, axis=3) # (28,28) -> (28,28,1)
return x
def preprocess_labels(y):
return y.reshape(-1, 1)
self.x_train = preprocess_imgs(self.x_train) # 훈련 데이터
self.y_train = preprocess_labels(self.y_train)
self.x_test = preprocess_imgs(self.x_test) # 테스트 데이터
self.y_test = preprocess_labels(self.y_test)
def batch_labeled(self, batch_size):
idx = np.random.randint(0, self.num_labeled, batch_size) # 레이블된 이미지와 레이블의 랜덤 배치 만들기
imgs = self.x_train[idx]
labels = self.y_train[idx]
return imgs, labels
def batch_unlabeled(self, batch_size):
idx = np.random.randint(self.num_labeled, self.x_train.shape[0], batch_size) # 레이블이 없는 이미지의 랜덤 배치 만들기
imgs = self.x_train[idx]
return imgs
def training_set(self):
x_train = self.x_train[range(self.num_labeled)]
y_train = self.y_train[range(self.num_labeled)]
return x_train, y_train
def test_set(self):
return self.x_test, self.y_test
여기서는 레이블된 MNIST 이미지 100개만 훈련에 사용한다.
num_labeled = 100 # 사용할 레이블된 샘플 개수
dataset = Dataset(num_labeled)
생성자
생성자 네트워크는 DCGAN에서 만든 것과 동일하다.
# 생성자
def build_generator(z_dim):
model = Sequential()
# 완전 연결층을 통해 입력을 7x7x256 크기 텐서로 변환
model.add(Dense(256 * 7 * 7, input_dim=z_dim))
model.add(Reshape((7, 7, 256)))
# 7x7x256에서 14x14x128 텐서로 바꾸는 전치 합성곱 층
model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding='same'))
# 배치 정규화
model.add(BatchNormalization())
# LeakyReLU 활성화 함수
model.add(LeakyReLU(alpha=0.01))
# 14x14x128에서 14x14x64 텐서로 바꾸는 전치 합성곱 층
model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding='same'))
# 배치 정규화
model.add(BatchNormalization())
# LeakyReLU 활성화 함수
model.add(LeakyReLU(alpha=0.01))
# 14x14x64에서 28x28x1 텐서로 바꾸는 전치 합성곱 층
model.add(Conv2DTranspose(1, kernel_size=3, strides=2, padding='same'))
# tanh 활성화 함수를 사용한 출력층
model.add(Activation('tanh'))
return model
판별자
판별자가 SGAN에서 가장 복잡하고 중요하다. SGAN 판별자는 두 가지 목표를 갖는다는 것을 기억하자.
진짜와 가짜 샘플을 구별한다. 이를 위해 SGAN 판별자는 시그모이드 함수를 사용해 이진 분류를 위한 하나의 확률을 출력한다. 진짜 샘플일 경우 레이블을 정확히 분류한다. 이를 위해 SGAN 판별자는 소프트 맥스 함수를 사용해 타깃 클래스마다 하나씩 확률을 출력한다.
<판별자 기반 모델>
먼저 판별자 네트워크의 핵심 부분을 정의한다. 이 부분은 4장에서 구현한 합성곱 기반의 판별자와 비슷하다. 그 다음에 드롭아웃 층을 추가한다. 이 층은 훈련하는 동안 신경망의 뉴런 사이 연결을 랜덤하게 꺼서 과대적합을 막는 데 도움을 준다. 이는 뉴런이 서로 의존하는 것을 방지하고 데이터에 내재된 더욱 일반적인 표현을 찾도록 만든다. 랜덤하게 끌 뉴런의 비율은 rate 매개변수로 지정한다. 여기서는 model.add(Dropout(0.5))와 같이 0.5로 지정했다. SGAN 분류 작업이 복잡하므로 드롭아웃을 추가하여 레이블된 샘플 100개에서 모델의 일반화 성능을 높이겠다.
def build_discriminator_net(img_shape):
model = Sequential()
model.add( # 28 x 28 x 1에서 14 x 14 x 32 텐서로 바꾸는 합성곱 층
Conv2D(32,
kernel_size=3,
strides=2,
input_shape=img_shape,
padding='same')
)
model.add(LeakyReLU(alpha=0.01)) # LeakyReLU 활성화 함수
model.add( # 14 x 14 x 32 에서 7 x 7 x 64 텐서로 바꾸는 합성곱 층
Conv2D(64,
kernel_size=3,
strides=2,
input_shape=img_shape,
padding='same')
)
model.add(LeakyReLU(alpha=0.01)) # LeakyReLU 활성화 함수
model.add( # 7 x 7 x 64에서 3 x 3 x 128 텐서로 바꾸는 합성곱 층
Conv2D(128,
kernel_size=3,
strides=2,
input_shape=img_shape,
padding='same')
)
model.add(LeakyReLU(alpha=0.01)) # LeakyReLU 활성화 함수
model.add(Dropout(0.5)) # 드롭아웃
model.add(Flatten()) # 텐서 펼치기
model.add(Dense(num_classes)) # num_classes 개의 뉴런을 가진 완전 연결 층
return model
앞의 신경망은 10개의 뉴런을 가진 완전 연결 층으로 끝난다. 그 다음 이 뉴런에서 두 개의 판별자 출력을 계산하는 신경망을 정의해야 한다. 하나는 지도 학습이고 (소프트맥스 함수로) 다중 분류를 수행한다. 다른 하나는 비지도 학습이고 (시그모이드 함수를 사용해) 이진 분류 한다.
<지도 학습 판별자>
def build_discriminator_supervised(discriminator_net):
model = Sequential()
model.add(discriminator_net) # 기존 분류기
model.add(Activation('softmax')) # 소프트맥스 활성화 함수
return model
<비지도 학습 판별자>
def build_discriminator_unsupervised(discriminator_net):
model = Sequential()
model.add(discriminator_net)
def predict(x):
prediction = 1.0 - (1.0 / (K.sum(K.exp(x), axis=-1, keepdims=True) + 1.0)) # 진짜 클래스에 대한 확률 분포를 진짜 대 가짜의 이진 확률로 변환한다.
return prediction
model.add(Lambda(predict)) # 앞서 정의한 진짜 대 가짜 확률을 출력하는 뉴런
return model
다음 코드는 판별자 기반 모델 위에 비지도 학습에 해당하는 판별자 모델을 만든다. predict(x) 함수는 10개 뉴런의 출력을 진짜 대 가짜의 이진 예측으로 변환한다.
GAN 모델 구성
판별자와 생성자 모델을 구성하고 컴파일한다. 지도 손실과 비지도 손실을 위해 categorical_crossentropy 와 binary_crossentropy 손실 함수를 사용한다.
def build_gan(generator, discriminator):
model = Sequential()
model.add(generator) # 생성자와 판별자 모델을 연결하기
model.add(discriminator)
return model
discriminator_net = build_discriminator_net(img_shape) # 판별자 기반 모델 : 이 층들은 지도 학습 훈련과 비지도 학습 훈련에 공유된다.
discriminator_supervised = build_discriminator_supervised(discriminator_net) # 지도 학습을 위해 판별자를 만들고 컴파일한다.
discriminator_supervised.compile(
loss='categorical_crossentropy',
optimizer=Adam(learning_rate = 0.003),
metrics=['accuracy']
)
discriminator_unsupervised = build_discriminator_unsupervised(discriminator_net) # 비지도 학습을 위해 판별자를 만들고 컴파일한다.
discriminator_unsupervised.compile(
loss='binary_crossentropy',
optimizer=Adam()
)
generator = build_generator(z_dim) # 생성자를 만든다.
discriminator_unsupervised.trainable = False # 생성자 훈련을 위해 판별자의 모델 파라미터를 동결한다.
gan = build_gan(generator, discriminator_unsupervised) # 새엇ㅇ자를 훈련하기 위해 고정된 판별자로 GAN 모델을 만들고 컴파일 한다. 비지도 학습용 판별자를 사용해야함.
gan.compile(loss='binary_crossentropy', optimizer=Adam())
훈련
SGAN 훈련 알고리즘
- (지도 학습) 판별자를 훈련한다.
- 레이블된 진짜 샘플 (x,y)의 랜덤 미니배치를 얻는다.
- 주어진 미니배치에 대한 D((x,y))를 계산하고 다중 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
- (비지도 학습) 판별자를 훈련한다.
- 레이블이 없는 진짜 샘플 x의 랜덤 미니배치를 얻는다.
- 주어진 미니배치에 대한 D(x)를 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
- 랜덤한 벡터 z의 미니배치를 얻어 가짜 샘플 G(z) = x*의 미니배치를 생성한다.
- 주어진 미니배치에 대한 D(x*)을 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
- 생성자를 훈련한다.
- 랜덤한 벡터 z의 미니배치를 얻어 가짜 샘플 G(z) = x*의 미니배치를 생성한다.
- 주어진 미니배치에 대한 D(x*)를 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
다음은 SGAN 훈련 알고리즘을 구현한 것이다.
supervised_losses = []
unsupervised_losses = []
def train(iterations, batch_size, sample_interval):
real = np.ones((batch_size, 1)) # 진짜 이미지의 레이블 : 모두 1
fake = np.zeros((batch_size, 1)) # 가짜 이미지의 레이블 : 모두 0
for iteration in range(iterations):
imgs, labels = dataset.batch_labeled(batch_size) # 레이블된 샘플을 가져온다.
labels = to_categorical(labels, num_classes=num_classes) # 레이블을 원-핫 인코딩한다.
imgs_unlabeled = dataset.batch_unlabeled(batch_size) # 레이블이 없는 샘플을 가져온다.
z = np.random.normal(0, 1, (batch_size, z_dim)) # 가짜 이미지의 배치를 생성한다.
gen_imgs = generator.predict(z) # 생성자
d_loss_supervised, accuracy = discriminator_supervised.train_on_batch(imgs, labels) # 레이블된 진짜 샘플에서 훈련한다.
d_loss_real = discriminator_unsupervised.train_on_batch(imgs, real) # 레이블이 없는 진짜 샘플에서 훈련한다.
d_loss_fake = discriminator_unsupervised.train_on_batch(gen_imgs, fake) # 레이블이 없는 가짜 샘플에서 훈련한다.
d_loss_supervised = 0.5 * np.add(d_loss_real, d_loss_fake)
z = np.random.normal(0, 1, (batch_size, z_dim)) # 가짜 이미지의 배치를 생성한다.
gen_imgs = generator.predict(z) # 생성자
g_loss = gan.train_on_batch(z, np.ones((batch_size,1))) # 생성자를 훈련
if (iteration + 1) % sample_interval == 0:
supervised_losses.append(d_loss_supervised)
unsupervised_losses.append(iteration+1)
print(
"%d [D 손실: %.4f, 정확도.: %.2f%%] [D 손실: %.4f] [G 손실: %f]"
% (iteration + 1, d_loss_supervised, 100 * accuracy, d_loss_unsupervised, g_loss)
)
<모델 훈련>
훈련을 위해 레이블된 샘플이 100개 뿐이므로 작은 배치 크기를 사용한다. 반복 횟수는 시행 착오를 거쳐 결정한다. 판별자의 지도 학습 손실 값이 평탄해질 때까지 이 횟수를 늘린다. 하지만 과대적합의 위험성을 줄이기 위해 이 지점을 너무 지나서까지 늘리진 않는다.
iterations = 8000 # 하이퍼파라미터를 세팅한다.
batch_size = 32
sample_interval = 800
train(iterations, batch_size, sample_interval) # 지정한 반복 횟수동안 SGAN을 훈련한다.
<모델 훈련과 테스트 정확도>
SGAN의 분류 정확도를 확인해보자. 지도 학습 정확도는 100%를 달한다. 왜냐하면 지도학습을 위해 레이블된 샘플 100개만 사용한다는 것을 잊으면 안된다. 아마 이 모델이 100개의 샘플을 모두 외웠을 것이다. 중요한 것은 이 분류기가 이전에 본 적 없는 훈련 데이터에 얼마나 잘 일반화할 수 있는가 이다.
x, y = dataset.test_set()
y = to_categorical(y, num_classes=num_classes)
_, accuracy = discriminator_supervised.evaluate(x, y) # 테스트 세트에서 분류 정확도 계산
print("테스트 정확도 : %.2f%%" % (100*accuracy))
지도 학습 분류기와 비교하기
가능한 공정하게 비교하기 위해 지도 학습 판별자 모델에 사용했던 것과 같은 구조를 사용해 완전한 지도 학습 분류기를 만들겠다. 이를 통해 GAN 방식의 준지도 학습으로 분류기의 능력이 얼마나 향상되었는지 구분할 수 있다.
# SGAN 판별자와 같은 네트워크 구조를 가진 지도 학습 분류기
mnist_classifier = build_discriminator_supervised(build_discriminator_net(img_shape))
mnist_classifier.compile(loss='categorical_crossentropy',
metrics=['accuracy'],
optimizer=Adam())
imgs, labels = dataset.training_set()
# 레이블을 원-핫 인코딩합니다.
labels = to_categorical(labels, num_classes=num_classes)
# 분류기를 훈련합니다.
training = mnist_classifier.fit(x=imgs,
y=labels,
batch_size=32,
epochs=30,
verbose=1)
losses = training.history['loss']
accuracies = training.history['accuracy']
SGAN 분류기처럼 완전 지도 학습 분류기는 훈련 데이터셋에서 100% 정확도를 달성한다. 하지만 테스트세드에서는 샘플의 약 70%만 정확하게 분류한다. SGAN보다 20%나 나쁘다.
훈련 데이터가 많으면 완전 지도 학습 분류기의 성능이 극정으로 향상된다. 동일한 조건과 훈련하에서 레이블된 샘플 10000개를 사용하는 완전 지도 학습 분류기는 98% 정확도를 달성한다. 하지만 이는 준지도 학습이라고 볼 수 없다.
결론
이 장에서는 준지도 학습을 위해 판별자가 진짜 샘플의 클래스 레이블을 출력하도록 GAN을 사용하는 방법에 대해 학습하였다. 적은 수의 훈련 샘플에서 SGAN으로 훈련한 분류기 성능이 완전한 지도 학습 분류기보다 더 뛰어나다는 것을 알 수 있었다.
머신 러닝에 대해 학습하면서 지도 학습이 과연 의미 있을까 하는 것에 의문이 있었다. 이 데이터셋이 어떤 것인지 레이블되어있으면 그 작업에 드는 시간과 비용은 어떻게 해결할 것인가.. 그런데 이번 장을 학습하면서 준지도 학습이라는 것을 처음 알게 되었다. 머신 러닝은 지도, 비지도, 강화 학습으로 분류되는 것으로 배웠지만 준지도 학습이라는 분야도 존재한다는 것을 알게 되면서 흥미로운 장이었던 것 같다. 어떻게 보면 비용적인 부분도 해결할 수 있다는 생각이 들었다
'Book > GAN In Action' 카테고리의 다른 글
| 8장. CGAN (0) | 2024.08.10 |
|---|---|
| 6장. ProGAN (0) | 2024.08.07 |
| 5장. GAN 평가의 어려움 (0) | 2024.08.06 |
| 4장. DCGAN (0) | 2024.08.01 |
| 3장. GAN 구현하기 (0) | 2024.07.30 |