오토인코더(Autoencoder)

오토인코더는 비지도방식으로 훈련된 인공신경망으로, 먼저 데이터에 인코딩 된 표현을 학습한 다음, 학습된 인코딩 표현에서 입력데이터를 가능한 가깝게 생성하는 것을 목표로 한다. 즉, 입력을 저차원 잠재 공간으로 인코딩한 후 디코딩하여 복원하는 네트워크이다.

 

다음은 오토 인코더의 구현 코드 입니다. Autoencoder의 클래스의 self.encoder는 인코더 모델을 나타내고, self.decoder는 디코더 모델을 나타냅니다.

class Flatten(torch.nn.Module): # 4D -> 2D로 계산하기
    def forward(self, x):
        batch_size = x.shape[0]
        return x.view(batch_size, -1) # (배치 수, 채널 수, 이미지 너비, 이미지 높이) -> (배치 수, 채널 수*이미지 너비*이미지 높이)

class Deflatten(nn.Module): # 2D -> 4D로 계산하기

    def __init__(self, k):
        super(Deflatten, self).__init__()
        self.k = k
        
    def forward(self, x):
        s = x.size()
        
        # 벡터 사이즈 = 채널 수*이미지 너비*이미지 높이
        # 벡터 사이즈 = 채널 수*이미지 사이즈**2
        # 이미지 사이즈 = (벡터 사이즈//채널 수)**.5
        feature_size = int((s[1]//self.k)**.5) 
        
        return x.view(s[0],self.k,feature_size,feature_size) # (배치 수, 채널 수*이미지 너비*이미지 높이) -> (배치 수, 채널 수, 이미지 너비, 이미지 높이) 
    

class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()
        
        k = 16
        self.encoder = nn.Sequential(
                        nn.Conv2d(1, k, 3, stride=2), 
                        nn.ReLU(), 
                        nn.Conv2d(k, 2*k, 3, stride=2),
                        nn.ReLU(), 
                        nn.Conv2d(2*k, 4*k, 3, stride=1),
                        nn.ReLU(),
                        Flatten(),
                        nn.Linear(1024, 10), 
                        nn.ReLU()
        )
        
        # ConvTranspose2d
        # 입력 성분(Conv의 결과)을 출력 성분(Conv의 입력)으로 미분하여 그 값을 입력 벡터와 곱해 출력 벡터를 산출한다.
        # 출력 된 벡터는 행렬 형태로 변환한다.
        self.decoder = nn.Sequential(
                        nn.Linear(10, 1024),
                        nn.ReLU(),
                        Deflatten(4*k),
                        nn.ConvTranspose2d(4*k, 2*k, 3, stride=1), # (입력 채널 수, 출력 채널 수, 필터 크기, stride)
                        nn.ReLU(),
                        nn.ConvTranspose2d(2*k, k, 3, stride=2),
                        nn.ReLU(),
                        nn.ConvTranspose2d(k, 1, 3, stride=2,output_padding=1),
                        nn.Sigmoid()
        )
    
    def forward(self, x):
        
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)

        return decoded

 

 

적대적 생성 신경망(GAN)

적대적 생성 신경망은 오토인코더와는 다른 방법으로 잠재 공간을 학습합니다. GAN을 직관적으로 이해하는 방법은 가짜 피카소 그림을 만드는 위조범을 생각하는 것입니다. 처음에 위조범은 형편없이 위조합니다. 진짜 피카소 그림과 위조품을 섞어서 그림 판매상에게 보여 줍니다. 판매상은 각 그림이 진짜인지 평가하고 어떤 것이 피카소 그림 같은지 위조범에게 피드백을 줍니다. 시간이 지남에 따라, 위조범은 피카소의 스타일을 모방하는 데 점점 더 능숙해질 것이고, 그림 판매상은 위조품을 구분하는데 더 전문가가 될 것입니다. 결국 진짜와 분류하기 어려운 훌륭한 피카소 위조품을 만들게 될것입니다. GAN에는 이와 같이 2개의 네트워크(generator network와 discriminator network)가 존재하고, 생성자 네트워크는 판별자 네트워크를 속이도록 훈련합니다. GAN은 최적화와 최솟값이 고정되지 않은 시스템으로, 최적화 과정이 최솟값을 찾는 것이 아니라 두힘 간의 평혐점을 찾는 다이나믹 시스템입니다. 이러한 이유로 GAN은 훈련하기 위해서는 모델 구조와 훈련 파라미터를 주의 깊게 많이 조정해야 합니다.

 

 

다음은 적대적 생성 신경망의 구현 코드 입니다. 해당 모델은 Vanila GAN으로 구현을 하였지만, Linear layer를 Convolutional Layer로 변환할 경우 DC-GAN으로 변형하여 적용 가능합니다. 해당 모델을 보면 ReLU함수를 사용하지 않고, LeakyReLU로 변경하여 선언하였는데, 이는 음수값은 미분했을경우 전부 0으로 나오기 때문에 dying-ReLU 현상이 발생할 수 있기 때문이다. LeakyReLU(0.2)의 경우 y=0.2x의 활성화 함수를 음수 범위에서 취한다는 뜻이다.

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.n_features = 128
        self.n_out = 784
        self.linear = nn.Sequential(
                    nn.Linear(self.n_features, 256),
                    nn.LeakyReLU(0.2),
                    nn.Linear(256, 512),
                    nn.LeakyReLU(0.2),
                    nn.Linear(512, 1024),
                    nn.LeakyReLU(0.2),
                    nn.Linear(1024, self.n_out),
                    nn.Tanh()
                    )
 
    def forward(self, x):
        x = self.linear(x)
        x = x.view(-1, 1, 28, 28)
        return x

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.n_in = 784
        self.n_out = 1
        self.linear = nn.Sequential(
                    nn.Linear(self.n_in, 1024),
                    nn.LeakyReLU(0.2),
                    nn.Dropout(0.3),
                    nn.Linear(1024, 512),
                    nn.LeakyReLU(0.2),
                    nn.Dropout(0.3),
                    nn.Linear(512, 256),
                    nn.LeakyReLU(0.2),
                    nn.Dropout(0.3),
                    nn.Linear(256, self.n_out),
                    nn.Sigmoid()
                    )
    def forward(self, x):
        x = x.view(-1, 784)
        x = self.linear(x)
        return x

 

적대적 생성 신경망에서는 두가지 손실함수를 정의 함과 동시에 최종적으로는 binary cross entropy를 사용한다. 학습 전략을 구현한 코드는 다음과 같다.

# 손실함수 및 최적화 방법 정의
generator = Generator().to(device)
discriminator = Discriminator().to(device)

g_optim = optim.Adam(generator.parameters(), lr=2e-4)
d_optim = optim.Adam(discriminator.parameters(), lr=2e-4)

g_losses = []
d_losses = []
images = []

criterion = nn.BCELoss()

def noise(n, n_features=128):
    return Variable(torch.randn(n, n_features)).to(device)

def label_ones(size):
    data = Variable(torch.ones(size, 1))
    return data.to(device)

def label_zeros(size):
    data = Variable(torch.zeros(size, 1))
    return data.to(device)
    
    
    # 학습 전략 정의
def train_discriminator(optimizer, real_data, fake_data):
    n = real_data.size(0)

    optimizer.zero_grad()
    
    prediction_real = discriminator(real_data)
    d_loss = criterion(prediction_real, label_ones(n))

    prediction_fake = discriminator(fake_data)
    g_loss = criterion(prediction_fake, label_zeros(n))
    
    loss = d_loss + g_loss

    loss.backward()
    optimizer.step()
    
    return loss.item()

def train_generator(optimizer, fake_data):
    n = fake_data.size(0)
    optimizer.zero_grad()
    
    prediction = discriminator(fake_data)
    # loss가 작다는 것은 discriminator가 구별을 잘못한다는 뜻
    loss = criterion(prediction, label_ones(n))
    
    loss.backward()
    optimizer.step()
    
    return loss.item()

'파이토치' 카테고리의 다른 글

[파이토치] Transfer Learning  (0) 2022.02.14
[파이토치] LSTM / GRU  (0) 2022.02.14
[파이토치] RNN  (0) 2022.02.14
[파이토치] CNN  (0) 2022.02.14
[파이토치] Cross-Validation  (0) 2022.02.14

## '컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커' 책을 공부하며 정리한 글입니다.

 

 

베어그런트 설치하기

베어그런트는 사용자의 요구에 맞게 시스템 자원을 할당, 배치. 배포해 두었다가 필요할 때 시스템을 사용할 수 있는 상태로 만들어 줍니다. 이를 프로비저닝(provisioning)이라고하는데, 프로비저닝을 하면 필요할 때 환경을 매우 쉽고 간단하게 구현할 수 있습니다. 실습을 위해 베이그런트를 2.0.3 버전을 설치해 보겠습니다.

 

wget -c https://releases.hashicorp.com/vagrant/2.0.3/vagrant_2.0.3_x86_64.deb
sudo dpkg -i vagrant_2.0.3_x86_64.deb

이때 guest machine과 VirtualBox host의 Guest Additions 버전이 다를 경우 mounting failed with the error 오류가 날 수 있는데, 이는 vagrant-vbguest 플러그인을 수동으로 설치함으로써 해결할 수 있습니다.

vagrant plugin install vagrant-vbguest

 

베어그런트로 랩 환경 구축하기

테스트 환경을 구성하기 전에 설치된 도구가 정상적으로 작동하는지 확인하기 위해, 프로비저닝을 위한 코드를 작성하고, 이를 베이그런트에서 불러온 후 버추얼박스에 운영 체제를 설치하겠습니다. 먼저, 베이그런트 초기화 명령을 실행해 비로비저닝에 필요한 기본 코드를 생성합니다.

vagrant init

 

생성된 Vagrantfile 스크립트 파일을 에디터 프로그램으로 열고 다음과 같이 수정합니다. 설정 목록은 크게 3가지로 나누어 집니다. 해당 네트워크 구성은 CentOS 기반 가상머신 3대에 기반하고 있습니다.

  1. 설치할 운영 체제의 이미지를 base에서 목적에 맞는 이미지로 변환하기 (sysnet4admin/CentOS-k8s)
  2. 호스트 전용 네트워크 설정 (192.168.1.10)
  3. 셸 프로비전을 통해 CentOS에 필요한 패키지 설치 (install_pkg.sh)
Vagrant.configure("2") do |config|
        config.vm.define "m-k8s" do |cfg|
                cfg.vm.box = "sysnet4admin/CentOS-k8s"
                cfg.vm.provider "virtualbox" do |vb|
                        vb.name = "m-k8s(github_SysNet4Admin)"
                        vb.cpus = 2
                        vb.memory = 2048
                        vb.customize ["modifyvm", :id, "--groups", "/k8s-SM(github_SysNet4Admin)"]
                end
                cfg.vm.host_name = "m-k8s"
                cfg.vm.network "private_network", ip: "192.168.1.10"
                cfg.vm.network "forwarded_port", guest: 22, host: 60010, auto_correct: true, id: "ssh"
                cfg.vm.synced_folder "../data", "/vagrant", disabled: true
                cfg.vm.provision "shell", path: "install_pkg.sh"
                cfg.vm.provision "file", source: "ping_2_nds.sh", destination: "ping_2_nds.sh"
                cfg.vm.provision "shell", path: "config.sh"
        end
        (1..3).each do |i|
                config.vm.define "w#{i}-k8s" do |cfg|
                        cfg.vm.box = "sysnet4admin/CentOS-k8s"
                        cfg.vm.provider "virtualbox" do |vb|
                                vb.name = "w#{i}-k8s(github_SysNet4Admin)"
                                vb.cpus = 1
                                vb.memory = 1024
                                vb.customize ["modifyvm", :id, "--groups", "/k8s-SM(github_SysNet4Admin)"]
                        end
                        cfg.vm.host_name = "w#{i}-k8s"
                        cfg.vm.network "private_network", ip: "192.168.1.10#{i}"
                        cfg.vm.network "forwarded_port", guest: 22, host: "6010#{i}", auto_correct:true, id: "ssh"
                        cfg.vm.synced_folder "../data", "/vagrant", disabled: true
                        cfg.vm.provision "shell", path: "install_pkg.sh"
                end
        end
  end

 

스크립트 파일 수정 이후, Vagrantfile에서 호출한 install_pkg.sh로 입력해 둔 배시 셸 파일을 실행해 EPEL 저장소와 코드 하이라이트를 위한 Vim의 추가 기능을 설치합니다.

#!/usr/bin/env bash
# install packages
yum install epel-release -y
yum install vim-enhanced -y

 

또한 ping 테스트 파일을 작성합니다.

# ping 3 times per nodes
ping 192.168.1.101 -c 3
ping 192.168.1.102 -c 3
ping 192.168.1.103 -c 3

 

마지막으로 권한 설정을 변경하는 스크립트를 작성합니다.

#!/usr/bin/env bash
# modify permission
chmod 744 ./ping_2_nds.sh

 

이제 4대의 가상 머신을 구성해 보겠습니다. 전체적인 테스트 환경의 구성은 밑의 그림과 같습니다. 추가한 프로비전 구문을 vagrant provision을 통해 실행하고, 앞서 작성한 파일들을 Vagrantfile과 같은 위치에 저장합니다. Vagrantfile을 읽어 들여 브로비저닝을 진행하면 가상 머신 이미지를 내려받습니다. 이때 주의해야할 점은, apt install virtualbox-qt를 통해서 선행적으로 virtualbox를 설치한 이후에 프로비저닝을 진행하여야 합니다. ssh를 통해 가상 머신에 접속한후 업로드된 ping_2_nds.sh파일을 실행해 3대의 CentOS(192.168.101~3)와 통신하는 데 문제가 없는지 확인합니다.

vagrant provision
vagrant up
vagrant ssh m-k8s
./ping_2_nds.sh

 

CentOS 3대를 추가로 구성한 테스트 환경

 

앞서 수정한 Vagrantfile로 설치된 추가 패키지를 yum repolist 명령으로 EPEL 저장소가 구성되었는지 그림과 같이 확인합니다. 또한, 문법 하이라이트가 적용되었는지 bashrc파일을 통해 확인한후 가상 머신에서 빠져나옵니다.

yum repolist
vi .bashrc
exit

yum repolist의 실행 결과

'쿠버네티스' 카테고리의 다른 글

[쿠버네티스] 01. 컨테이너 인프라 환경이란?  (0) 2022.02.11

전이학습(Transfer Learning)

전이학습이란 기존의 잘 알려진 데이터 혹은 사전학습 된 모델을 업무 효율 증대나 도메인 확장을 위해 사용하는 학습을 의미한다. 모델의 초기 low level layer는 general한 feature를 추출하고 high level에서는 specific한 특징을 추출해내는 고도화된 학습이 이루어진다. 따라서 초기 layer의 feature들은 학습할 때 재사용해도 되지만 후반부에 위치한 layer의 feature들은 재학습이 필요하다. 본 실습은 CIFAR-10 데이터를 이용하였다.

10개의 클래스로 이루어진 CIFAR-10 dataset

 

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

import torch.nn as nn
import torch.optim as optim
# 데이터 불러오기 및 전처리 작업
transform = transforms.Compose(
    [transforms.RandomCrop(32, padding=4), # data augmentation 기법: 이미지 한장에서 일부를 crop한다.
     transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

test_transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=16, shuffle=True) 

testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=16,shuffle=False)

 

 

다음은 ResNet18 불러오겠습니다. pretrained=True를 하면 ResNet18 구조와 사전 학습 된 파라메타를 모두 불러오고, pretrained=False를 하면 ResNet18 구조만 불러옵니다. 이때 주의해야할 점은 모델과 텐서에 .to(device)를 붙여야만 GPU 연산이 가능하니 꼭 기입해야합니다.

model = torchvision.models.resnet18(pretrained=True)
print(model)
모델의 구조를 보면 마지막 출력 노드가 1000개라는 것을 알 수 있는데요, 이는 1000개의 클래스를 가진 ImageNet 데이터를 이용하여 사전학습 된 모델이기 때문입니다. 따라서 우리가 사용하는 CIFAR10 데이터에 맞게 출력층의 노드를 10개로 변경해야만 합니다.

기존의 모델
num_ftrs = model.fc.in_features # fc의 입력 노드 수를 산출한다. 512개
model.fc = nn.Linear(num_ftrs, 10) # fc를 nn.Linear(num_ftrs, 10)로 대체한다.
model = model.to(device)
print(model)
fc에서 out_feature가 10으로 변한것을 알 수 있다.

 

이어서 loss function과 optimizer를 정의하여 학습을 수행합니다. 코드는 다음과 같습니다.

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-2)
for epoch in range(20):

    running_loss = 0.0
    for data in trainloader:
        
        inputs, labels = data[0].to(device), data[1].to(device)
          
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()

    cost = running_loss / len(trainloader)        
    print('[%d] loss: %.3f' %(epoch + 1, cost))  

torch.save(model.state_dict(), './models/cifar10_resnet18.pth')      

print('Finished Training')
model = torchvision.models.resnet18(pretrained=False) # 모델의 구조를 먼저 불러오고 pre-trained된 파라메터를 덮어씌운다.
num_ftrs = model.fc.in_features # fc의 입력 노드 수를 산출한다. 512개
model.fc = nn.Linear(num_ftrs, 10) # fc를 nn.Linear(num_ftrs, 10)로 대체한다.
model = model.to(device)
model.load_state_dict(torch.load('./models/cifar10_resnet18.pth'))
correct = 0
total = 0
with torch.no_grad():
    model.eval()
    for data in testloader:
        images, labels = data[0].to(device), data[1].to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))

모델 동결(Model Freezing)

 

피쳐 추출에 해당하는 합성곱 층의 변수를 업데이트 하지 않고 분류 파트에 해당하는 fully connected layer의 변수만 업데이트 할 수 있는데 이 때 변수가 업데이트 되지 않게 변수를 얼린다고 하여 이를 프리징(Freezing)이라고 합니다. 먼저 AlexNet 구조와 사전 학습 된 파라메타를 모두 불러오겠습니다. 모델의 구조를 보면 마지막 출력 노드가 1000개라는 것을 알 수 있는데요, 이는 1000개의 클래스를 가진 ImageNet 데이터를 이용하여 사전학습 된 모델이기 때문입니다. 따라서 이전 예제와 같이 우리가 사용하는 CIFAR10 데이터에 맞게 출력층의 노드를 10개로 변경해야만 합니다. 다음 모델은 AlexNet과 동일한 타입의 이미지 분류 문제를 위해 사용하였으므로, feature extraction층에 존재하는 가중치 값들은 동결하고 classifier층에 존재하는 가중치만을 재조정하여 customize된 작업에 사용할 수 있다.

model = torchvision.models.alexnet(pretrained=True)

num_ftrs = model.classifier[6].in_features # fc의 입력 노드 수를 산출한다. 
model.classifier[6] = nn.Linear(num_ftrs, 10) # fc를 nn.Linear(num_ftrs, 10)로 대체한다.
model = model.to(device)
# 합성곱 층은 0~9까지이다. 따라서 9번째 변수까지 역추적을 비활성화 한 후 for문을 종료한다.

for i, (name, param) in enumerate(model.named_parameters()):
    
    param.requires_grad = False
    if i == 9:
        print('end')
        break
# requires_grad 확인
print(model.features[0].weight.requires_grad) # False
print(model.features[0].bias.requires_grad) # False
print(model.features[3].weight.requires_grad) # False
print(model.features[3].bias.requires_grad) # False
print(model.features[6].weight.requires_grad) # False
print(model.features[6].bias.requires_grad) # False
print(model.features[8].weight.requires_grad) # False
print(model.features[8].bias.requires_grad) # False
print(model.features[10].weight.requires_grad) # False
print(model.features[10].bias.requires_grad) # False
print(model.classifier[1].weight.requires_grad) # True
print(model.classifier[1].bias.requires_grad) # True
print(model.classifier[4].weight.requires_grad) # True
print(model.classifier[4].bias.requires_grad) # True
print(model.classifier[6].weight.requires_grad) # True
print(model.classifier[6].bias.requires_grad) # True
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-2)
for epoch in range(20):

    running_loss = 0.0
    for data in trainloader:
        
        inputs, labels = data[0].to(device), data[1].to(device)
          
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()

    cost = running_loss / len(trainloader)        
    print('[%d] loss: %.3f' %(epoch + 1, cost))  
   

print('Finished Training')

'파이토치' 카테고리의 다른 글

[파이토치] Unsupervised Learning  (0) 2022.02.15
[파이토치] LSTM / GRU  (0) 2022.02.14
[파이토치] RNN  (0) 2022.02.14
[파이토치] CNN  (0) 2022.02.14
[파이토치] Cross-Validation  (0) 2022.02.14
  • There are two other popular recurrent layer: LSTM and GRU.
  • SimpleRNN is difficult to learn long-term dependencies.
    • This is due to the vanishing gradient problem.
  • Long Short-Term Memory (LSTM)
    • proposed by Hochreiter and Schmidhuber in 1997
    • It adds a way to carry information across many timesteps.

Details of LSTM

 

LSTM diagram
Computations involved in LSTM


class LSTM(nn.Module):
    
    def __init__(selfinput_sizehidden_sizesequence_lengthnum_layersdevice):
        super(LSTM, self).__init__()
        self.device = device
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size*sequence_length, 1)
        
    def forward(selfx):
        h0 = torch.zeros(self.num_layers, x.size()[0], self.hidden_size).to(self.device)
        c0 = torch.zeros(self.num_layers, x.size()[0], self.hidden_size).to(self.device) # cell state가 추가되었다.
        out, _ = self.lstm(x, (h0, c0)) # output, (hn, cn): cell state와 hidden state만 반환 (순서쌍 형태로)
        out = out.reshape(out.shape[0], -1# <- state 추가
        out = self.fc(out)
        return out

GRU &amp;nbsp;diagram
Computations involved in GRU


class GRU(nn.Module):
    
    def __init__(selfinput_sizehidden_sizesequence_lengthnum_layersdevice):
        super(GRU, self).__init__()
        self.device = device
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size*sequence_length, 1)
        
    def forward(selfx):
        h0 = torch.zeros(self.num_layers, x.size()[0], self.hidden_size).to(self.device)
        out, _ = self.gru(x, h0)
        out = out.reshape(out.shape[0], -1# <- state 추가
        out = self.fc(out)
        return out
 

GRU 모델을 이용한 예측 그래프

'파이토치' 카테고리의 다른 글

[파이토치] Unsupervised Learning  (0) 2022.02.15
[파이토치] Transfer Learning  (0) 2022.02.14
[파이토치] RNN  (0) 2022.02.14
[파이토치] CNN  (0) 2022.02.14
[파이토치] Cross-Validation  (0) 2022.02.14

+ Recent posts