Generative Model (생성형 모델) 중 하나인 ChatGPT가 AI의 잠재력을 전세계에 퍼뜨리고 있다. 개인적인 생각으로 만든 사람도 정확히 ChatGPT가 어떻게 학습이 가능한 지에 대한 설명을 할 수는 없다고 생각한다. Model의 layer, weight들이 학습되는 형태는 처음에 초기값을 어떻게 잡는 지, 어떤 activation function을 쓰는 지에 따라 다르며, chatGPT처럼 데이터가 많게 되면 더더욱 설명하기는 어렵다. 우리가 ChatGPT의 원리를 정확히 알 수는 없더라도 생성형 모델의 기본적인 유형 중 하나인 Autoencoder에 대해서 먼저 보도록 하자.
Autoencoder는 encoder 부분과 decoder 부분으로 나뉜다. Encoder는 latent space(h)로 input data를 압축(추상화) 시켜서차원을 압축한다. Decoder는 latent space를 다시 압축 해제하여 원래 input data를 출력한다. latent space는 manifold를 이루며 autoencoder의 학습이 끝나면 나중에는 latent space를 decoder에만 넣어서 원하는 결과를 출력 할 수 있다. 대표적인 예시로, VAE, GAN, Stable-Diffusion 등등 이미지 분야에서 다양하게 쓰인다.
이 블로그에서 다루는 autoencoder는 28 x 28 의 mnist 숫자 데이터이다. pixel 값에 따라 input dimension과 output dimension이 달라지니 개인적인 데이터를 사용할 시에는 참고 바란다!
Autoencoder 의 구성
Autoencoder class를 만든다. Activation Function은 sigmoid function을 사용하고 순전파(forward propagation), 역전파 (backward propagation) method, training method를 정의한다.
Initialize Weight
weight은 Gaussian Distribution을 따르고 평균은 0, 표준편차는 weight의 input layer의 dimension의 제곱근이다. weight의 사이즈는 input dimesion x output dimension인 행렬이다. 현재는 프레임워크나 라이브러리 없이 해서 간단한 Gaussian Distribution으로 했지만, Xavier Initialization을 사용하여 많은 데이터에 적용할 수 있다.
import numpy as np
import matplotlib.pyplot as plt
class Autoencoder:
def __init__(self, input_dim, hidden_dim, lr=0.01):
self.weight1 = np.random.normal(0.0, pow(input_dim, -0.5), (input_dim, hidden_dim))
self.weight2 = np.random.normal(0.0, pow(hidden_dim, -0.5), (hidden_dim, input_dim))
self.hidden = np.zeros((1,hidden_dim))
self.learningrate = lr
self.input_dim = input_dim
self.hidden_dim = hidden_dim
self.theta = 0
def sigmoid(self, x):
return 1/(1+np.exp(-x))
def feedforward(self, x):
self.h = self.sigmoid(np.dot(x, self.weight1) - self.theta)
return self.sigmoid(np.dot(self.h, self.w2)-self.theta)
def backprop_weight2(self, t, y): # target, output
temporary = -2*(t-y)*y*(1-y)
return np.dot(self.h.reshape(self.hidden_dim, 1), temporary.reshape(1, self.input_dim))
def backprop_weight1(self, t, y, x): # target, output, input
temporary1 = -2*(t-y)*y*(1-y)
temporary2 = np.dot(self.weight2, temporary1)
return np.dot(x.reshape(self.input_dim, 1), temporary2*self.h*(1-self.h).reshape(1,self.hidden_dim))
def training(self, input, target):
x = np.array(input).T
y = self.feedforward(x)
t = np.array(target).T
self.weight1 = self.weight1 - self.lr*self.backprop_weight1(t,y,x)
self.weight2 = self.weight2 - self.lr*self.backprop_weight2(t,y)
Autoencoder Training
모델을 학습 시키기 전에 hyperparameter를 설정한다. 우리가 사용하는 이미지는 784 픽셀의 이미지이며, latent space의 차원은 100으로 하고 epoch(반복 횟수)는 100으로 한다. 마지막 줄을 통해, 모델 class를 담은 객체를 생성한다!
# Training
input_dim = 784
hidden_dim = 100
epoch = 100
ae = Autoencoder(784, 100, lr=0.1)
모델 학습을 위한 이미지 데이터 셋 다운로드. 여기서는 인터넷에서 흔히 구할 수 있는 MNIST data를 사용한다. 따로 데이터를 첨부하지는 않으며 인터넷에 찾으면 무척 많다.
training_dataset_file = open("/content/drive/MyDrive/ml/mnist_original.csv", 'r')
training_dataset_list = training_dataset_file.readlines()
training_dataset_file.close()
input_list = list()
각 이미지들을 위에서 만든 ae 라는 autoencoder 객체를 통해서 학습한다.
for k in range(epoch):
ae.lr = ae.lr * 0.8 # learning rate decay
for i in training_dataset_list:
all_values = i.split(',')
inputs = (np.asfarray(all_values[1:])/255.0*0.99) + 0.01
input_list.append(inputs)
ae.training(inputs, inputs)
참고로 training 동안에는 숫자 0만을 학습 시켰다!!
결과 확인
실제로 이미지를 출력해보면서 결과를 확인해보자.
원본 이미지 출력
# True image (dimension = 784)
im_array = np.asfarray(input_list[1]).reshape((28,28))
plt.imshow(im_array, cmap="Greys", interpolation='None')
Autoencoder가 생성한 이미지
output = ae.feedforward(input_list[1])
im_array = np.asfarray(output).reshape((28,28))
plt.imshow(im_array, cmap="Greys", interpolation='None') # change 'epoch'
학습에 사용한 숫자 0 외에 다른 숫자 출력해보기
학습동안 사용된 숫자 0은 autoencoder가 출력하는 게 신기하지 않다. 그렇다면 autoencoder 학습에 사용되지 않은 다른 숫자들을 넣었을 때는 어떻게 될까?
테스트에 사용할 데이터 다운로드
test_dataset_file = open("/content/drive/MyDrive/ml/mnist_noise.csv", 'r')
test_dataset_list = test_dataset_file.readlines()
test_dataset_file.close()
test_inputs_list = list()
for i in test_dataset_list:
all_values = i.split(',')
test_inputs = (np.asfarray(all_values[1:])/255*0.99) + 0.01
test_inputs_list.append(test_inputs)
원본 이미지 출력
im_array = np.asfarray(test_inputs_list[1]).reshape((28,28))
plt.imshow(im_array, cmap='Greys', interpolation='None')
Autoencoder가 생성한 이미지
노이즈가 낀 데이터에 대한 Autoencoder 테스트
Autoencoder의 작동 원리상 latent space를 한번 거치기 때문에 이미지가 추상화(압축) 된다. 사람이 기억을 할 때 필요한 기억만 남고 불필요한 기억들은 없애는 것처럼, autoencoder도 동일하게 작동한다.
원본 이미지 출력
im_array = np.asfarray(test_inputs_list[20]).reshape((28,28))
plt.imshow(im_array, cmap='Greys', interpolation='None')
Autoencoder가 생성한 이미지
output = ae.feedforward(test_inputs_list[20])
im_array = np.asfarray(output).reshape((28,28))
plt.imshow(im_array, cmap='Greys', interpolation='None')
Autoencoder의 기본적인 원리를 알아보고 코드로 구현해 보았다! ChatGpt, Stable Diffusion, Midjourney 등 다양한 Generative Model도 비슷한 원리를 따라간다. 여기서 어떤 weight initialization, activation function, regularization, model type 을 쓸 건지에 따라서 성능 차이가 생길 뿐이다. 이미지를 생성하냐, 말을 생성하냐에 따라서 사용한는 모델 또한 바뀌니 신기 방귀 뿡 가이다!