GAN 설명

GAN은 일반적인 신경망 학습과 달리 생성 모델 G(generator)와 식별 모델 D(discriminator)를 사용하여 상호 간 학습을 진행한다. G는 k차원의 잠재적 특이 벡터를 입력값으로 받아서 대상(ex. 64 x 64)과 동일 형식의 데이터를 생성하는 신경망이다. D는 지금까지 본 것과 마찬 가지로 대상 데이터를 입력값으로 받아 진위를 식별하는 신경망이다. GAN의 학습 순서를 수식을 사용하지 않고 설명해 보겠다.

 

    1. 잠재적 특이 벡터 z를 난수로 생성하고, G(z)를 사용해 가짜 데이터(fake_data)를 생성

    2. fake_data를 D로 식별

    3. 진짜 데이터의 샘플(real_data)을 D로 식별한다. 

    4. fake_out이 진짜 데이터(1)라고 간주하고, 크로스 엔트로피 함수를 계산해서 G의 파라미터를 갱신한다.

    5. real_out이 진짜 데이터이고, fake_out이 가짜 데이터(0)라고 간주하고, 크로스 엔트로피 함수를 계산해서 D의 파라미터를 갱신한다.

 

이 처럼 서로를 훈련 시키는 것이 GAN의 핵심이다. G나 D에 깊은 CNN을 사용한 것이 DCGAN이다.

 

필요한 데이터를 다운로드 합니다.

!wget http://www.robots.ox.ac.uk/~vgg/data/flowers/102/102flowers.tgz
!tar xf 102flowers.tgz
!mkdir oxford-102
!mkdir oxford-102/jpg
!mv jpg/*.jpg oxford-102/jpg

필요한 라이브러리를 다운로드 합니다.

import torch
from torch import nn, optim
from torch.utils.data import (Dataset, DataLoader, TensorDataset)
import tqdm

from torchvision.datasets import ImageFolder
from torchvision import transforms


from torchvision.utils import save_image

 

디렉터리와 파일 준비가 완료 되었고, DataLoader를 만듭니다. (Pytorch 전용 DataLoader) 여기서는 64 X 64픽셀의 이미지를 생성하므로 데이터의 짧은 변을 80픽셀로 조절한 후, 가운데를 64 X 64픽셀로 잘랐습니다.

img_data = ImageFolder("oxford-102/",
    transform=transforms.Compose([
        transforms.Resize(80),
        transforms.CenterCrop(64),
        transforms.ToTensor()
]))

batch_size = 64
img_loader = DataLoader(img_data, batch_size=batch_size,
                        shuffle=True)

 

이미지 생성 모델

잠재적 특이 벡터 z를 100 차원으로 구성하고 이 z로 부터 3 X 64 X 64(3은 채널)의 이미지를 만드는 생성 모델을 구축한다.

Transposed Convolution을 총 5회 반복하고 있다. 이를 통해 처음에는 100 X 1 X 1 의 z가 256 X 4 X 로 변환되며 최종으로 3 X 64 X 64가 됩니다. 

nz = 100
ngf = 32
class GNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.main = nn.Sequential(
            nn.ConvTranspose2d(nz, ngf * 8, 
                               4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(ngf * 8, ngf * 4,
                               4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(ngf * 4, ngf * 2,
                               4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(ngf * 2, ngf,
                               4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(ngf, 3,
                               4, 2, 1, bias=False),
            nn.Tanh()
        )
        
    def forward(self, x):
        out = self.main(x)
        return out
# 이미지 크기 
out_size = (in_size - 1) * stride -2 * padding \ + kernel_size + output_padding


첫 번째 Transposed Convolution
in_size = 1
stride = 1
padding = 0
kernel_size = 4
output_padding = 0

 

식별 모델 작성

3 X 64 X 64 이미지를 최종적으로는 1차원의 스칼라로 변환하는 신경망을 만든다.

5회 합성곱 연산으로 3 X 64 X 64 이미지가 최종적으로 1 X 1 X 1이 된다. 그리고 forward 마지막에 있는 squeeze는 A X 1 X B X 1 처럼 불필요한 1이 들어 있는 shape를 A X B로 조정하는 처리이다.

ndf = 32

class DNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.main = nn.Sequential(
            nn.Conv2d(3, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
        )
    
    def forward(self, x):
        out = self.main(x)
        return out.squeeze()

Model Train

fixed_z는 훈련 모니터링용이고 훈련이 진행됨에 따라 어떤 이미지가 만들어지는지 확인한다.

from statistics import mean

def train_dcgan(g, d, opt_g, opt_d, loader):
    # 생성 모델, 식별 모델의 목적 함수 추적용 배열
    log_loss_g = []
    log_loss_d = []
    for real_img, _ in tqdm.tqdm(loader):
        batch_len = len(real_img)
        
        # 실제 이미지를 GPU로 복사
        real_img = real_img.to("cuda:0")
        
        # 가짜 이미지를 난수와 생성 모델을 사용해 만든다
        z = torch.randn(batch_len, nz, 1, 1).to("cuda:0")
        fake_img = g(z)
        
        # 나중에 사용하기 위해서 가짜 아미지의 값만 별도로 저장해둠
        fake_img_tensor = fake_img.detach()
        
        # 가짜 이미지에 대한 생성 모델의 평가 함수 계산
        out = d(fake_img)
        loss_g = loss_f(out, ones[: batch_len])
        log_loss_g.append(loss_g.item())

        # 계산 그래프가 생성 모델과 식별 모델 양쪽에
        # 의존하므로 양쪽 모두 경사하강을 끝낸 후에
        # 미분 계산과 파라미터 갱신을 실시
        d.zero_grad(), g.zero_grad()
        loss_g.backward()
        opt_g.step()
        
        #실제 이미지에 대한 식별 모델의 평가 함수 계산
        real_out = d(real_img)
        loss_d_real = loss_f(real_out, ones[: batch_len])
        
        # PyTorch에선 동일 Tensor를 포함한 계산 그래프에
        # 2회 backward를 할 수 없으므로 저장된 Tensor를
        # 사용해서 불필요한 계산은 생략
        fake_img = fake_img_tensor
        
        # 가짜 아미지에 대한 식별 모델의 평가 함수 계산
        fake_out = d(fake_img_tensor)
        loss_d_fake = loss_f(fake_out, zeros[: batch_len])
        
        # 진위 평가 함수의 합계
        loss_d = loss_d_real + loss_d_fake
        log_loss_d.append(loss_d.item())
        
        # 식별 모델의 미분 계산고 파라미터 갱신
        d.zero_grad(), g.zero_grad()
        loss_d.backward()
        opt_d.step()
                                             
    return mean(log_loss_g), mean(log_loss_d)
for epoch in range(300):
    train_dcgan(g, d, opt_g, opt_d, img_loader)
    # 10회 반복마다 학습 결과를 저장
    if epoch % 10 == 0:
        # 파라미터 저장
        torch.save(
            g.state_dict(),
            "g_{:03d}.prm".format(epoch),
            pickle_protocol=4)
        torch.save(
            d.state_dict(),
            "d_{:03d}.prm".format(epoch),
            pickle_protocol=4)
        # 모니터링용 z로부터 생성한 이미지 저장
        generated_img = g(fixed_z)
        save_image(generated_img,
                   "{:03d}.jpg".format(epoch))

10 에폭마다 결과물이 저장된다.

DCGAN을 통해 생성된 이미지

처음 생성된 이미지를 예시로 가져와 흐릿하지만 모델 후반으로 갈 수록 생성된 이미지가 선명한 것을 확인할 수 있을 것이다.

 

BERT fine-tuning method for downstream tasks

파인 튜닝은 BERT를 처음부터 학습시키지 않는다는 것을 의마한다. 그 대신 사전 학습된 BERT를 기반으로 태스크에 맞게 가중치를 업데이트 하게된다.
이로써 Kaggle, Dacon과 같은 NLP_competition에서 가장 중요한 작업이라고 할 수 있을 것이다. (최근 몇년 NLP계열 S.O.T.A는 BERT가 싹쓸이하기 때문)

총 4가지의 다운스트림 태스크를 다룰 예정이다.

  1. 텍스트 분류
  2. 자연어 추론
  3. 개체명 인식
  4. 질문 - 응답

1. 텍스트 분류

사전 학습된 BERT 모델을 텍스트 분류 태스크에 맞춰 파인 튜닝하는 방법이다. 감정 분석을 수행하고 있다고 가정해보겠다.
감정 분석 태스크에서 목표는 문장이 긍정인지 부정인지 분류하는 것이다. 레이블과 함께 문장이 포함된 데이터셋이 있다고 가정한다.

'I love paris'라는 문장이 주어졌을때 먼저 문장을 토큰화하고 시작 부분에[CLS]토큰의 임베딩만 취한다. [CLS]토큰을 추가한 뒤
문장 끝에 [SEP]토큰을 추가한다. 그 다음 사전 학습된 BERT 모델에 대한 입력으로 토큰을 입력하고 모든 토큰의 임베딩을 가져온다.

다른 모든 토큰의 임베딩을 무시하고 R[CLS](CLS토큰의 임베딩 값)인 [CLS]토큰의 임베딩만 취한다. [CLS]토큰을 포함하면 문장의
집계 표현이 유지된다.R[CLS]를 분류기(소프트맥스 함수가 있는 피드포워드 네트워크)에 입력하고 학습시켜 감정 분석을 수행한다.

스크린샷 2022-05-20 오후 1 03 16



이러한 그림으로 표현될 수 있을 것 같다.

2. 자연어 추론

자연어 추론에서 모델은 가정이 주진 전제에 참인지 거짓인지 중립인지 여부를 결정하는 태스크다.

 



먼저 문자 쌍을 토큰화한 다음 첫번째 문장의 시작 부분에 [CLS]토큰을 추가하고 모든 문장 끝에 [SEP]토큰을 추가한다.
후에 사전 학습된 BERT에 토큰을 입력하고 각 토큰의 임베딩을 가져온다. [CLS] 토큰의 표현이 집계 표현을 보유한다.

따라서 [CLS]토큰의 R[CLS](CLS토큰의 임베딩 값) 표현 벡터를 가져와 분류기(피드포워드 + 소프트맥스)에 입력하면, 분류기는 문장이 참, 거짓,
중립일 확률을 반환한다. 학습 초기에는 결과가 정확하지 않지만 여러 번 반복하면 정확한 결과를 얻을 수 있다.

3. 질문-응답

질문 - 응답 태스크에서는 질문에 대한 응답이 포함된 단락과 함께 질문이 제공된다. 태스크의 목표는 주어진 질문에 대한 단락에서 답을 추출하는 것이다.

BERT의 입력은 질문-단락 쌍이다. 즉, BERT에 질문과 응답을 담은 단락을 입력하고 단락에서 응답을 추출해야 한다. 따라서 BERT는 단락에서 응답에
해당하는 텍스트의 범위를 반환해야 한다.

4. 개체명 인식

개체명 인식에서 우리의 목표는 개체명을 미리 정의된 범주로 분류하는 것이다. 예를들어 '영찬이는 서울에 산다'라는 문장을 예로 들어보면 이 문장에서
'영찬'은 사람으로, '서울'은 위치로 분류한다.

먼저 문장을 토큰화한 다음 시작 부분에[CLS]토큰을 추가하고 끝에 [SEP]토큰을 추가한다. 그런 다음 사전 학습된 BERT 모델에 토큰을 입력하고 모든
토큰의 표현 벡터를 얻는다. 그리고 이러한 토큰 표현을 분류기(피드포워드 네트워크 + 소프트맥스 함수)에 입력한다. 그러면 분류기는 개체명이 속한 범주를
반환한다.

스크린샷 2022-05-20 오후 5 37 55

 

다중공선성문제는 통계학의 회귀분석에서 독립변수들 간에 강한 상관관계가 나타나는 문제이다. 독립변수들간에 정확한 선형관계가 존재하는 완전공선성의 경우와 독립변수들간에 높은 선형관계가 존재하는 다중공선성으로 구분하기도 한다.

 

 

이진 분류에 자주 사용되는 모델인 로지스틱 회귀모형을 다루게 되면 필히 다뤄야할 내용이다.

이번 블로깅은 다중공선성 문제를 해결해 나가는 방법에 대해 다뤄보겠다.

 

# WDBC 데이터셋의 diagnosis 진단 변수는 목표변수(반응변수, 종속변수, Output 변수)로서 
# 2개의 class (악성 Malignant, 양성 Benign)를 가진 범주형 자료로서, 30개의 연속형 설명변수를 
# 사용해서 2개의 class (악성, 양성 여부)를 분류(classification) 하는 해야합니다. 
# 30개의 설명변수를 사용하여 유방암 악성(Malignant)에 속할 0~1 사이의 확률값을 계산하여 진단하는 분류/예측 모델을 만들어보겠습니다.
# https://www.kaggle.com/uciml/breast-cancer-wisconsin-data    데이터 주소
data <- read.csv("C:/Users/inp032/Desktop/회사자료/Ai 프로젝트/data.csv")

data
head(data)
data.frame(data)
str(data)
summary(data)

#M,B 종속변수 값을 1,0 으로 바꿔줌
Y <- ifelse(data$diagnosis == 'M', 1, 0)

X <- data[,c(3:32)]

 

데이터는 30개의 컬럼으로 이루어져 있고 총 569개의 데이터가 존재한다. 

 

 

VIF(lm(radius_mean ~ .,data=data))
VIF(lm(texture_mean ~ .,data=data))

require(fmsb)
vif_func <- function(in_frame,thresh=10, trace=F,...){
  require(fmsb)
  if(class(in_frame) != 'data.frame') in_frame<-data.frame(in_frame)
  vif_init <- vector('list', length = ncol(in_frame))
  names(vif_init) <- names(in_frame)
  var_names <- names(in_frame)
  
  for(val in var_names){
    regressors <- var_names[-which(var_names == val)]
    form <- paste(regressors, collapse = '+')
    form_in <- formula(paste(val,' ~ .'))
    vif_init[[val]] <- VIF(lm(form_in,data=in_frame,...))
  }
  vif_max<-max(unlist(vif_init))
  
  if(vif_max < thresh){
    
    if(trace==T){ #print output of each iteration
      prmatrix(vif_init,collab=c('var','vif'),rowlab=rep('', times = nrow(vif_init) ),quote=F)
      cat('\n')
      cat(paste('All variables have VIF < ', thresh,', max VIF ',round(vif_max,2), sep=''),'\n\n')
    }
    return(names(in_frame))
  }
  else{
    in_dat<-in_frame
    #backwards selection of explanatory variables, stops when all VIF values are below 'thresh'
    while(vif_max >= thresh){
      
      vif_vals <- vector('list', length = ncol(in_dat))
      names(vif_vals) <- names(in_dat)
      var_names <- names(in_dat)
      
      for(val in var_names){
        regressors <- var_names[-which(var_names == val)]
        form <- paste(regressors, collapse = '+')
        form_in <- formula(paste(val,' ~ .'))
        vif_add <- VIF(lm(form_in,data=in_dat,...))
        vif_vals[[val]] <- vif_add
      }
      max_row <- which.max(vif_vals)
      
      #max_row <- which( as.vector(vif_vals) == max(as.vector(vif_vals)) )
      vif_max<-vif_vals[max_row]
      
      if(vif_max<thresh) break
      
      if(trace==T){ #print output of each iteration
        vif_vals <- do.call('rbind', vif_vals)
        vif_vals
        prmatrix(vif_vals,collab='vif',rowlab=row.names(vif_vals),quote=F)
        cat('\n')
        cat('removed: ', names(vif_max),unlist(vif_max),'\n\n')
        flush.console()
      }
      in_dat<-in_dat[,!names(in_dat) %in% names(vif_max)]
    }
    return(names(in_dat))
  }
}

 

 

for문을 이용하여 함수를 만들었다. 다중공선성은 보통 분산팽창지수인 vif값이 10이상인 경우 다중공선성이 심각하다고 판단 된다. 내가 for문을 이용한 이유는 제일 먼저 vif값이 높은 변수를 지우고 그 다음 다시 vif값을 계산해서 다시 제일 높은 vif값을 가지고 있는 변수를 제거하는 방법이다. 10이상인 값이 존재하지 않을때 for문은 멈추게 된다.

 

 

함수를 이제 대입시켜 준다.

#다중공선성이 높은 컬럼 지워주기
data_custom <- vif_func(X, thresh=10, trace=T) 
#남은 데이터 갯수 확인하기
length(data_custom)

X_2 <- X[, data_custom]

 

 

결과

반복 되고 마지막엔 30개에서 13개가 지워져서 17개의 데이터만 남았다.

+ Recent posts