Main Content

이 번역 페이지는 최신 내용을 담고 있지 않습니다. 최신 내용을 영문으로 보려면 여기를 클릭하십시오.

변분 오토인코더(VAE)를 훈련시켜 영상 생성

이 예제에서는 MATLAB에서 변분 오토인코더(VAE)를 만들어 숫자 영상을 생성하는 방법을 보여줍니다. VAE는 MNIST 데이터 세트의 스타일로 손으로 그린 숫자를 생성합니다.

VAE는 입력값을 복원하는 데 인코딩-디코딩 절차를 사용하지 않는다는 점에서 일반 오토인코더와 다릅니다. 대신 잠재 공간에 확률 분포를 적용하고, 디코더 출력값의 분포가 관측된 데이터의 분포와 일치하도록 분포를 학습합니다. 그런 다음 이 분포에서 샘플링하여 새 데이터를 생성합니다.

이 예제에서는 VAE 신경망을 생성하고, 이를 MNIST 데이터 세트에 대해 훈련시키고, 데이터 세트에 있는 영상과 매우 비슷한 새 영상을 생성합니다.

데이터 불러오기

http://yann.lecun.com/exdb/mnist/에서 MNIST 파일을 다운로드하고 MNIST 데이터 세트를 작업 공간에 불러옵니다[1]. 이 예제에 첨부된 processImagesMNIST 헬퍼 함수와 processLabelsMNIST 헬퍼 함수를 호출하여 파일의 데이터를 MATLAB 배열로 불러옵니다.

VAE는 복원된 숫자를 범주형 레이블이 아닌 입력값과 비교하므로 MNIST 데이터 세트의 훈련 레이블을 사용할 필요가 없습니다.

trainImagesFile = 'train-images-idx3-ubyte.gz';
testImagesFile = 't10k-images-idx3-ubyte.gz';
testLabelsFile = 't10k-labels-idx1-ubyte.gz';

XTrain = processImagesMNIST(trainImagesFile);
Read MNIST image data...
Number of images in the dataset:  60000 ...
numTrainImages = size(XTrain,4);
XTest = processImagesMNIST(testImagesFile);
Read MNIST image data...
Number of images in the dataset:  10000 ...
YTest = processLabelsMNIST(testLabelsFile);
Read MNIST label data...
Number of labels in the dataset:  10000 ...

신경망 생성하기

오토인코더에는 인코더와 디코더의 두 가지 부분이 있습니다. 인코더는 영상 입력값을 받아서 압축된 표현을 출력합니다(인코딩). 압축된 표현은 크기가 latentDim(이 예제에서는 20)인 벡터입니다. 디코더는 압축된 표현을 받아서 디코딩한 다음 원본 영상을 다시 만듭니다.

보다 수치적으로 안정적인 계산을 수행하려면 신경망이 분산의 로그로부터 학습하도록 만들어서 가능한 값의 범위를 [0,1]에서 [-inf, 0]으로 늘리십시오. 크기가 latent_dim인 벡터를 두 개 만듭니다. 하나는 평균 μ용이고, 다른 하나는 분산의 로그 log(σ2)용입니다. 그런 다음 두 벡터를 사용하여 샘플을 추출할 분포를 만듭니다.

2차원 컨벌루션 계층과 그 뒤에 오는 완전 연결 계층을 사용하여 28×28×1 MNIST 영상에서 잠재 공간의 인코딩으로 다운샘플링합니다. 그런 다음 전치된 2차원 컨벌루션을 사용하여 1×1×20 인코딩을 다시 28×28×1 영상으로 확장합니다.

latentDim = 20;
imageSize = [28 28 1];

encoderLG = layerGraph([
    imageInputLayer(imageSize,'Name','input_encoder','Normalization','none')
    convolution2dLayer(3, 32, 'Padding','same', 'Stride', 2, 'Name', 'conv1')
    reluLayer('Name','relu1')
    convolution2dLayer(3, 64, 'Padding','same', 'Stride', 2, 'Name', 'conv2')
    reluLayer('Name','relu2')
    fullyConnectedLayer(2 * latentDim, 'Name', 'fc_encoder')
    ]);

decoderLG = layerGraph([
    imageInputLayer([1 1 latentDim],'Name','i','Normalization','none')
    transposedConv2dLayer(7, 64, 'Cropping', 'same', 'Stride', 7, 'Name', 'transpose1')
    reluLayer('Name','relu1')
    transposedConv2dLayer(3, 64, 'Cropping', 'same', 'Stride', 2, 'Name', 'transpose2')
    reluLayer('Name','relu2')
    transposedConv2dLayer(3, 32, 'Cropping', 'same', 'Stride', 2, 'Name', 'transpose3')
    reluLayer('Name','relu3')
    transposedConv2dLayer(3, 1, 'Cropping', 'same', 'Name', 'transpose4')
    ]);

사용자 지정 훈련 루프를 사용하여 두 신경망을 훈련시키고 자동 미분을 활성화하려면 계층 그래프를 dlnetwork 객체로 변환하십시오.

encoderNet = dlnetwork(encoderLG);
decoderNet = dlnetwork(decoderLG);

모델 기울기 함수 정의하기

헬퍼 함수 modelGradients는 인코더 및 디코더 dlnetwork 객체와 입력 데이터로 구성된 미니 배치 X를 받습니다. 그런 다음 신경망의 학습 가능한 파라미터에 대한 손실의 기울기를 반환합니다. 이 헬퍼 함수는 이 예제의 마지막 부분에서 정의됩니다.

함수는 샘플링과 손실이라는 두 단계로 이 절차를 수행합니다. 샘플링 단계에서는 평균 벡터와 분산 벡터를 샘플링하여 디코더 신경망에 전달할 최종 인코딩을 만듭니다. 하지만 무작위 샘플링 연산을 통한 역전파는 가능하지 않기 때문에 재파라미터화 기법을 사용해야 합니다. 이 기법은 무작위 샘플링 연산을 보조 변수 ε으로 옮깁니다. 보조 변수는 평균 μi만큼 이동되고 표준편차 σi로 스케일링됩니다. 이 단계의 요점은 N(μi,σi2)에서 샘플링하는 것과 μi+εσi에서 샘플링하는 것이 같다는 것입니다. 여기서 εN(0,1)입니다. 다음 그림은 이 요점을 시각적으로 묘사합니다.

손실 단계는 샘플링 단계에서 생성된 인코딩을 디코더 신경망에 통과시켜서 손실을 확인한 다음, 확인한 손실을 사용하여 기울기를 계산합니다. ELBO(Evidence Lower Bound) 손실이라고도 하는 VAE의 손실은 다음과 같이 두 개별 손실 항의 합으로 정의됩니다.

ELBO loss=reconstruction loss+KL loss.

복원 손실은 다음과 같이 평균 제곱 오차(MSE)를 사용하여 디코더 출력값이 원래 입력값과 얼마나 비슷한지 측정합니다.

reconstruction loss=MSE(decoder output,original image).

KL 손실(Kullback–Leibler 발산)은 두 확률 분포 간의 차이를 측정합니다. 이 경우에 KL 손실을 최소화한다는 것은 학습된 평균 및 분산을 대상 분포(정규분포)의 평균 및 분산에 가능한 한 가까워지게 한다는 것을 의미합니다. 크기가 n인 잠재 차원에 대해 KL 손실은 다음과 같이 구해집니다.

KL loss=-0.5i=1n(1+log(σi2)-μi2-σi2).

KL 손실 항을 포함하는 것의 실용적인 효과는 잠재 공간의 중앙을 중심으로 복원 손실로 인해 학습된 클러스터를 빽빽하게 채워 넣어 샘플을 추출할 연속 공간을 형성한다는 것입니다.

훈련 옵션 지정하기

사용 가능한 GPU가 있으면 GPU에서 훈련시킵니다(Parallel Computing Toolbox™ 필요).

executionEnvironment = "auto";

신경망의 훈련 옵션을 지정합니다. Adam 최적화 함수를 사용하는 경우에는 각 신경망에 대해 후행 평균 기울기 감쇠율과 후행 평균 기울기 제곱 감쇠율을 빈 배열로 초기화해야 합니다.

numEpochs = 50;
miniBatchSize = 512;
lr = 1e-3;
numIterations = floor(numTrainImages/miniBatchSize);
iteration = 0;

avgGradientsEncoder = [];
avgGradientsSquaredEncoder = [];
avgGradientsDecoder = [];
avgGradientsSquaredDecoder = [];

모델 훈련시키기

사용자 지정 훈련 루프를 사용하여 모델을 훈련시킵니다.

Epoch의 각 반복에 대해 다음을 수행합니다.

  • 훈련 세트에서 다음 미니 배치를 가져옵니다.

  • 차원 레이블을 각각 공간(spatial), 공간(spatial), 채널(channel), 배치(batch)를 의미하는 'SSCB'로 지정하여 미니 배치를 dlarray 객체로 변환합니다.

  • GPU 훈련을 위해 dlarraygpuArray 객체로 변환합니다.

  • dlfeval 함수와 modelGradients 함수를 사용하여 모델 기울기를 평가합니다.

  • adamupdate 함수를 사용하여 두 신경망에 대해 신경망 학습 가능 파라미터와 평균 기울기를 업데이트합니다.

각 Epoch의 끝에서, 테스트 세트 영상을 오토인코더에 통과시킨 다음 해당 Epoch에 대한 손실 및 훈련 시간을 표시합니다.

for epoch = 1:numEpochs
    tic;
    for i = 1:numIterations
        iteration = iteration + 1;
        idx = (i-1)*miniBatchSize+1:i*miniBatchSize;
        XBatch = XTrain(:,:,:,idx);
        XBatch = dlarray(single(XBatch), 'SSCB');
        
        if (executionEnvironment == "auto" && canUseGPU) || executionEnvironment == "gpu"
            XBatch = gpuArray(XBatch);           
        end 
            
        [infGrad, genGrad] = dlfeval(...
            @modelGradients, encoderNet, decoderNet, XBatch);
        
        [decoderNet.Learnables, avgGradientsDecoder, avgGradientsSquaredDecoder] = ...
            adamupdate(decoderNet.Learnables, ...
                genGrad, avgGradientsDecoder, avgGradientsSquaredDecoder, iteration, lr);
        [encoderNet.Learnables, avgGradientsEncoder, avgGradientsSquaredEncoder] = ...
            adamupdate(encoderNet.Learnables, ...
                infGrad, avgGradientsEncoder, avgGradientsSquaredEncoder, iteration, lr);
    end
    elapsedTime = toc;
    
    [z, zMean, zLogvar] = sampling(encoderNet, XTest);
    xPred = sigmoid(forward(decoderNet, z));
    elbo = ELBOloss(XTest, xPred, zMean, zLogvar);
    disp("Epoch : "+epoch+" Test ELBO loss = "+gather(extractdata(elbo))+...
        ". Time taken for epoch = "+ elapsedTime + "s")    
end
Epoch : 1 Test ELBO loss = 28.0145. Time taken for epoch = 28.0573s
Epoch : 2 Test ELBO loss = 24.8995. Time taken for epoch = 8.797s
Epoch : 3 Test ELBO loss = 23.2756. Time taken for epoch = 8.8824s
Epoch : 4 Test ELBO loss = 21.151. Time taken for epoch = 8.5979s
Epoch : 5 Test ELBO loss = 20.5335. Time taken for epoch = 8.8472s
Epoch : 6 Test ELBO loss = 20.232. Time taken for epoch = 8.5068s
Epoch : 7 Test ELBO loss = 19.9988. Time taken for epoch = 8.4356s
Epoch : 8 Test ELBO loss = 19.8955. Time taken for epoch = 8.4015s
Epoch : 9 Test ELBO loss = 19.7991. Time taken for epoch = 8.8089s
Epoch : 10 Test ELBO loss = 19.6773. Time taken for epoch = 8.4269s
Epoch : 11 Test ELBO loss = 19.5181. Time taken for epoch = 8.5771s
Epoch : 12 Test ELBO loss = 19.4532. Time taken for epoch = 8.4227s
Epoch : 13 Test ELBO loss = 19.3771. Time taken for epoch = 8.5807s
Epoch : 14 Test ELBO loss = 19.2893. Time taken for epoch = 8.574s
Epoch : 15 Test ELBO loss = 19.1641. Time taken for epoch = 8.6434s
Epoch : 16 Test ELBO loss = 19.2175. Time taken for epoch = 8.8641s
Epoch : 17 Test ELBO loss = 19.158. Time taken for epoch = 9.1083s
Epoch : 18 Test ELBO loss = 19.085. Time taken for epoch = 8.6674s
Epoch : 19 Test ELBO loss = 19.1169. Time taken for epoch = 8.6357s
Epoch : 20 Test ELBO loss = 19.0791. Time taken for epoch = 8.5512s
Epoch : 21 Test ELBO loss = 19.0395. Time taken for epoch = 8.4674s
Epoch : 22 Test ELBO loss = 18.9556. Time taken for epoch = 8.3943s
Epoch : 23 Test ELBO loss = 18.9469. Time taken for epoch = 10.2924s
Epoch : 24 Test ELBO loss = 18.924. Time taken for epoch = 9.8302s
Epoch : 25 Test ELBO loss = 18.9124. Time taken for epoch = 9.9603s
Epoch : 26 Test ELBO loss = 18.9595. Time taken for epoch = 10.9887s
Epoch : 27 Test ELBO loss = 18.9256. Time taken for epoch = 10.1402s
Epoch : 28 Test ELBO loss = 18.8708. Time taken for epoch = 9.9109s
Epoch : 29 Test ELBO loss = 18.8602. Time taken for epoch = 10.3075s
Epoch : 30 Test ELBO loss = 18.8563. Time taken for epoch = 10.474s
Epoch : 31 Test ELBO loss = 18.8127. Time taken for epoch = 9.8779s
Epoch : 32 Test ELBO loss = 18.7989. Time taken for epoch = 9.6963s
Epoch : 33 Test ELBO loss = 18.8. Time taken for epoch = 9.8848s
Epoch : 34 Test ELBO loss = 18.8095. Time taken for epoch = 10.3168s
Epoch : 35 Test ELBO loss = 18.7601. Time taken for epoch = 10.8058s
Epoch : 36 Test ELBO loss = 18.7469. Time taken for epoch = 9.9365s
Epoch : 37 Test ELBO loss = 18.7049. Time taken for epoch = 10.0343s
Epoch : 38 Test ELBO loss = 18.7084. Time taken for epoch = 10.3214s
Epoch : 39 Test ELBO loss = 18.6858. Time taken for epoch = 10.3985s
Epoch : 40 Test ELBO loss = 18.7284. Time taken for epoch = 10.9685s
Epoch : 41 Test ELBO loss = 18.6574. Time taken for epoch = 10.5241s
Epoch : 42 Test ELBO loss = 18.6388. Time taken for epoch = 10.2392s
Epoch : 43 Test ELBO loss = 18.7133. Time taken for epoch = 9.8177s
Epoch : 44 Test ELBO loss = 18.6846. Time taken for epoch = 9.6858s
Epoch : 45 Test ELBO loss = 18.6001. Time taken for epoch = 9.5588s
Epoch : 46 Test ELBO loss = 18.5897. Time taken for epoch = 10.4554s
Epoch : 47 Test ELBO loss = 18.6184. Time taken for epoch = 10.0317s
Epoch : 48 Test ELBO loss = 18.6389. Time taken for epoch = 10.311s
Epoch : 49 Test ELBO loss = 18.5918. Time taken for epoch = 10.4506s
Epoch : 50 Test ELBO loss = 18.5081. Time taken for epoch = 9.9671s

결과 시각화하기

결과를 시각화하고 해석하려면 헬퍼 시각화 함수를 사용하십시오. 이러한 헬퍼 함수는 이 예제의 마지막 부분에서 정의됩니다.

VisualizeReconstruction 함수는 각 클래스에서 무작위로 선택한 숫자와 이 숫자를 오토인코더에 통과시킨 후의 복원 결과를 보여줍니다.

VisualizeLatentSpace 함수는 테스트 영상을 인코더 신경망에 통과시킨 후에 생성된 평균 인코딩과 분산 인코딩(각각 차원이 20)을 받아서 각 영상에 대한 인코딩을 포함하는 행렬에 대해 주성분 분석(PCA)을 수행합니다. 그런 다음, 처음 두 개의 주성분으로 특징지어지는 두 차원에서 평균과 분산으로 정의되는 잠재 공간을 시각화할 수 있습니다.

Generate 함수는 정규분포에서 샘플링된 새 인코딩을 초기화하고, 이러한 인코딩이 디코더 신경망을 통과할 때 생성되는 영상을 출력합니다.

visualizeReconstruction(XTest, YTest, encoderNet, decoderNet)

visualizeLatentSpace(XTest, YTest, encoderNet)

generate(decoderNet, latentDim)

다음 단계

변분 오토인코더는 생성적 작업을 수행하는 데 사용되는 여러 모델 중 하나에 불과합니다. 변분 오토인코더는 영상이 작고 명확하게 정의된 특징을 갖는 데이터 세트(예: MNIST)에서 잘 작동합니다. 더 큰 영상을 갖는 보다 복잡한 데이터 세트의 경우에는 생성적 적대 신경망(GAN)의 성능이 더 낫고 더 적은 잡음으로 영상을 생성합니다. GAN을 구현하여 64×64 RGB 영상을 생성하는 방법을 보여주는 예제는 생성적 적대 신경망(GAN) 훈련시키기 항목을 참조하십시오.

참고 문헌

  1. LeCun, Y., C. Cortes, and C. J. C. Burges. "The MNIST Database of Handwritten Digits." http://yann.lecun.com/exdb/mnist/.

헬퍼 함수

모델 기울기 함수

modelGradients 함수는 인코더 및 디코더 dlnetwork 객체와 입력 데이터로 구성된 미니 배치 X를 받습니다. 그런 다음 신경망의 학습 가능한 파라미터에 대한 손실의 기울기를 반환합니다. 함수는 다음과 같은 세 가지 연산을 수행합니다.

  1. 인코더 신경망을 통과하는 영상 미니 배치에 대해 sampling 함수를 호출하여 인코딩을 구합니다.

  2. 인코딩을 디코더 신경망에 통과시키고 ELBOloss 함수를 호출하여 손실을 구합니다.

  3. dlgradient 함수를 호출하여 두 신경망의 학습 가능한 파라미터에 대한 손실의 기울기를 계산합니다.

function [infGrad, genGrad] = modelGradients(encoderNet, decoderNet, x)
[z, zMean, zLogvar] = sampling(encoderNet, x);
xPred = sigmoid(forward(decoderNet, z));
loss = ELBOloss(x, xPred, zMean, zLogvar);
[genGrad, infGrad] = dlgradient(loss, decoderNet.Learnables, ...
    encoderNet.Learnables);
end

샘플링 및 손실 함수

sampling 함수는 입력 영상으로부터 인코딩을 얻습니다. 먼저, 영상의 미니 배치를 인코더 신경망에 통과시키고 크기가 (2*latentDim)*miniBatchSize인 출력값을 평균으로 구성된 행렬과 분산으로 구성된 행렬로 분할합니다. 여기서 두 행렬은 각각 크기가 latentDim*batchSize입니다. 그런 다음 두 행렬을 사용하여 재파라미터화 기법을 구현하고 인코딩을 계산합니다. 마지막으로, 이 인코딩을 SSCB 형식의 dlarray 객체로 변환합니다.

function [zSampled, zMean, zLogvar] = sampling(encoderNet, x)
compressed = forward(encoderNet, x);
d = size(compressed,1)/2;
zMean = compressed(1:d,:);
zLogvar = compressed(1+d:end,:);

sz = size(zMean);
epsilon = randn(sz);
sigma = exp(.5 * zLogvar);
z = epsilon .* sigma + zMean;
z = reshape(z, [1,1,sz]);
zSampled = dlarray(z, 'SSCB');
end

ELBOloss 함수는 sampling 함수가 반환하는 평균과 분산의 인코딩을 받으며, 이 인코팅을 사용하여 ELBO 손실을 계산합니다.

function elbo = ELBOloss(x, xPred, zMean, zLogvar)
squares = 0.5*(xPred-x).^2;
reconstructionLoss  = sum(squares, [1,2,3]);

KL = -.5 * sum(1 + zLogvar - zMean.^2 - exp(zLogvar), 1);

elbo = mean(reconstructionLoss + KL);
end

시각화 함수

VisualizeReconstruction 함수는 MNIST 데이터 세트의 각 숫자에 대해 두 개의 영상을 무작위로 선택하고 이를 VAE에 통과시킨 다음 원래 영상과 복원 결과를 나란히 플로팅합니다. dlarray 객체에 포함된 정보를 플로팅하려면 먼저 extractdata 함수와 gather 함수를 사용하여 추출해야 합니다.

function visualizeReconstruction(XTest,YTest, encoderNet, decoderNet)
f = figure;
figure(f)
title("Example ground truth image vs. reconstructed image")
for i = 1:2
    for c=0:9
        idx = iRandomIdxOfClass(YTest,c);
        X = XTest(:,:,:,idx);

        [z, ~, ~] = sampling(encoderNet, X);
        XPred = sigmoid(forward(decoderNet, z));
        
        X = gather(extractdata(X));
        XPred = gather(extractdata(XPred));

        comparison = [X, ones(size(X,1),1), XPred];
        subplot(4,5,(i-1)*10+c+1), imshow(comparison,[]),
    end
end
end

function idx = iRandomIdxOfClass(T,c)
idx = T == categorical(c);
idx = find(idx);
idx = idx(randi(numel(idx),1));
end

VisualizeLatentSpace 함수는 인코더 신경망의 출력값을 형성하는, 평균 행렬과 분산 행렬로 정의되는 잠재 공간을 시각화하고 각 숫자의 잠재 공간 표현으로 형성되는 클러스터를 찾습니다.

함수는 먼저 dlarray 객체에서 평균 행렬과 분산 행렬을 추출합니다. 채널/배치 차원(C 및 B)을 갖는 행렬을 전치하는 것은 가능하지 않으므로 함수는 stripdims를 호출한 후에 행렬을 전치합니다. 그런 다음 두 행렬에 대해 주성분 분석(PCA)을 수행합니다. 잠재 공간을 두 차원에서 시각화하기 위해, 함수는 처음 두 개의 주성분을 유지하고 이를 나란히 플로팅합니다. 마지막으로, 함수는 사용자가 클러스터를 관찰할 수 있도록 숫자 클래스에 색을 입힙니다.

function visualizeLatentSpace(XTest, YTest, encoderNet)
[~, zMean, zLogvar] = sampling(encoderNet, XTest);

zMean = stripdims(zMean)';
zMean = gather(extractdata(zMean));

zLogvar = stripdims(zLogvar)';
zLogvar = gather(extractdata(zLogvar));

[~,scoreMean] = pca(zMean);
[~,scoreLogvar] = pca(zLogvar);

c = parula(10);
f1 = figure;
figure(f1)
title("Latent space")

ah = subplot(1,2,1);
scatter(scoreMean(:,2),scoreMean(:,1),[],c(double(YTest),:));
ah.YDir = 'reverse';
axis equal
xlabel("Z_m_u(2)")
ylabel("Z_m_u(1)")
cb = colorbar; cb.Ticks = 0:(1/9):1; cb.TickLabels = string(0:9);

ah = subplot(1,2,2);
scatter(scoreLogvar(:,2),scoreLogvar(:,1),[],c(double(YTest),:));
ah.YDir = 'reverse';
xlabel("Z_v_a_r(2)")
ylabel("Z_v_a_r(1)")
cb = colorbar;  cb.Ticks = 0:(1/9):1; cb.TickLabels = string(0:9);
axis equal
end

generate 함수는 VAE의 생성적 기능을 테스트합니다. 함수는 무작위로 생성된 25개의 인코딩을 포함하는 dlarray 객체를 초기화하고 이를 디코더 신경망에 통과시킨 다음 출력값을 플로팅합니다.

function generate(decoderNet, latentDim)
randomNoise = dlarray(randn(1,1,latentDim,25),'SSCB');
generatedImage = sigmoid(predict(decoderNet, randomNoise));
generatedImage = extractdata(generatedImage);

f3 = figure;
figure(f3)
imshow(imtile(generatedImage, "ThumbnailSize", [100,100]))
title("Generated samples of digits")
drawnow
end

참고 항목

| | | | | |

관련 항목