AutoEncoder

AutoEncoder는 학습 데이터에 레이블 데이터를 별도로 구축할 필요가 없는, 주어진 데이터만으로 학습이 가능한 비지도 학습 신경망입니다. 엄밀히 말해 입력 데이터가 곧 레이블 데이터가 됩니다. AutoEncoder 신경망은 Encoder와 Decoder라는 2개의 신경망으로 구성됩니다. Encoder는 입력 데이터에서 중요한 정보만을 남기고 신경망의 입장에서 학습시에 중요하지 않다고 판단되는 정보는 제거함으로써 처음 입력 데이터의 크기보다 더 작은 크기의 데이터(z)를 생성해 주는 신경망입니다. Decoder는 Encoder가 생성한 z를 가지고 다시 처음의 입력 이미지로 복원하는 신경망입니다.

Encoder가 생성해 주는 보다 더 작은 크기의 데이터를 잠재 벡터(Latent Vector)이라고 하며, z라고 흔히 표기합니다. 이 z는 GAN의 Generator의 입력 데이터인 z와 그 의미가 동일선상에 놓여 있습니다. 잠재 벡터라고 하는 이유는 어떤 중요한 ‘의미’가 잠재되어 있는 데이터(벡터)이기 때문입니다. 결국 이 z에는 처음 입력 데이터에서 중요한 의미만을 남겨 놓은 것, 압축된 것이라고 할 수 있습니다. 또한 이 z에는 별로 중요하지 않거나 잡음같은 것들이 제거된 데이터라고 할 수 있습니다. 압축 차원에서 보자면 손실 압축입니다. 이러한 AutoEncoder 신경망의 용도는 차원감소, 중요한 의미 추출, 잠재벡터를 통한 복잡한 데이터의 공간상 시각화, 이미지 검색, Segmentation, Super Resolution 등 매우 다양합니다.

이러한 AutoEncoder를 이미지 압축과 복원의 관점에서 CNN 레이어를 사용해 구현함으로써 더욱 구체적인 내용을 정리해 보겠습니다. 딥러닝 라이브러리는 PyTorch를 사용하였습니다. TensorFlow 역시 신경망 구성 레이어는 동일하니 어렵지 않게 변환이 가능할 것입니다.

먼저 필요한 패키지들을 Import 해 둡니다.

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torchvision.datasets as dset
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import random
from torch.utils.data import DataLoader

압축과 복원 대상이 되는 이미지는 Fashion MNIST를 사용하겠습니다.

batch_size = 1024
root = './MNIST_Fashion'
transform = transforms.Compose([transforms.ToTensor()])
train_data = dset.FashionMNIST(root=root, train=True, transform=transform, download=True)
test_data = dset.FashionMNIST(root=root, train=False, transform=transform, download=True)

train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True, drop_last=True)
test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False, drop_last=True)

AutoEncoder의 신경망에 대한 클래스를 정의합니다.

z_size = 314

class AutoEncoder(nn.Module):
    def __init__(self):
        super(AutoEncoder, self).__init__()

        self.encoder = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=32, kernel_size=2, stride=2, bias=False),
            nn.LeakyReLU(),
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=2, stride=2, bias=False),
            nn.LeakyReLU(),
            Reshape((-1,7*7*64)),
            nn.Linear(7*7*64, z_size),
            nn.LeakyReLU(),
        )

        self.decoder = nn.Sequential(
            nn.Linear(z_size, 7*7*64),
            nn.LeakyReLU(),
            Reshape((-1,64,7,7)),
            nn.ConvTranspose2d(in_channels=64, out_channels=32, kernel_size=2, stride=2, bias=False),
            nn.LeakyReLU(),
            nn.ConvTranspose2d(in_channels=32, out_channels=1, kernel_size=2, stride=2, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        out = self.encoder(x)
        out = self.decoder(out)
        return out

위의 코드가 기술하는 신경망의 구성 레이어들 사이로 전달되는 Tensor의 크기를 표기한 그림은 다음과 같습니다.

AutoEncoder 클래스는 encoder와 decoder로 레이어들을 분리해 구성하고 있는 것을 볼 수 있습니다. 이렇게 해두면 추후에 encoder 만을 이용해 잠재 벡터만을 쉽게 얻어낼 수 있습니다. 보시면 encoder와 decoder는 레이어의 구성이 성호 대칭성이 있는 것을 볼 수 있습니다. 또한 앞서 언급 했듯이 AutoEncoder는 입력 데이터가 레이블 데이터로 사용되므로 출력 데이터가 입력 데이터의 텐서 크기와 동일합니다. 중요한 점은 z의 크기인 z_size를 314로 해두었는데, 이는 원본 이미지의 크기 28×28인 784가 약 40%의 크기로 압축된다는 것입니다. 다시 상기하면 이 40% 크기로 압축된 z에는 원본 이미지의 중요한 특징값들이 잠재되어 있고 불필요한 것이라고 판단되는 것들은 제거되어 있다는 것입니다. 물론 이러한 판단은 신경망이 데이터를 통해 스스로 판단합니다.

추가적으로 위의 신경망 클래스의 구성 레이어 중 Reshape라는 Tensor의 크기를 변경해 주는 레이어의 코드는 다음과 같습니다.

class Reshape(nn.Module):
    def __init__(self, shape):
        super(Reshape, self).__init__()
        self.shape = shape

    def forward(self, x):
        return x.view(*self.shape)

다음은 학습 코드입니다.

num_epochs = 15

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = AutoEncoder().to(device)
loss_func = nn.MSELoss().to(device)
optimizer = optim.Adam(model.parameters())
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer,threshold=0.1, patience=1, mode='min')    

for i in range(num_epochs):
    for _, [image, _] in enumerate(train_loader):
        x = image.to(device)
        y_= image.to(device)
        
        optimizer.zero_grad()
        output = model(x)
        loss = loss_func(output, y_)
        loss.backward()
        optimizer.step()
 
    scheduler.step(loss)      
    print('Epoch: {}, Loss: {}, LR: {}'.format(i, loss.item(), scheduler.optimizer.state_dict()['param_groups'][0]['lr']))

몇차례 실행을 해보니 15 Epoch 정도에서 손실값이 더 이상 감소하지 않는 것을 확인했고 Overfitting을 방지하기 위해 최종적으로 반복 학습수를 15로 지정했습니다. 물론 어떤 신경망은 학습시에 손실값이 한동안 일정값에서 정체 하다가 다시 감소하는 경우가 있으므로 지속적이고 세밀한 관찰이 필요합니다.

학습이 완료되었다면, 그 결과를 시각화합니다.

model.eval()

rows = 4
for c in range(rows):
    plt.subplot(rows, 2, c*2+1)
    rand_idx = random.randint(0, test_data.data.shape[0])
    plt.imshow(test_data.data[rand_idx].view(28,28), cmap='gray')
    plt.axis('off')

    plt.subplot(rows, 2, c*2+2)
    inp = transform(test_data.data[rand_idx].numpy().reshape(28,28)).reshape(1,1,28,28).to(device)
    img = model(inp)
    plt.imshow(img.view(28,28).detach().cpu().numpy(), cmap='gray')
    plt.axis('off')

    print(test_data.targets[rand_idx])

plt.show()

결과는 아래와 같습니다.

총 4줄의 이미지들이 표시되는데, 왼쪽은 입력 이미지이고 오른쪽은 AutoEncoder의 Decoder가 생성해낸 이미지입니다.

Proj4js

이 글은 proj4js.org 사이트에서 제공되는 내용을 파악하기 위해 정리한 포스트이며, 좀 더 상세한 내용을 추가적으로 담고자 노력하였습니다.

Proj4js는 좌표계 간의 상호 변환하기 위한 자바스크립트 라이브러리이며 서로 다른 타원체 간의 Datum 변환 기능을 포함하고 있습니다. 이 라이브러리는 원래 C언어로 개발된 PROJ.4와 MetaCRS 그룹의 프로젝트 중의 하나인 GCTCP C를 JavaScript로 포팅한 것입니다.

설치

개발자의 개발환경에 따라 다르지만, 아래의 4가지 방식 중 한가지 방식으로 설치가 가능합니다.

npm install proj4
bower install proj4
jam install proj4
component install proj4js/proj4js

또는 최신 배포의 dist/ 폴더에서 proj4.js 파일을 직접 사용할 수 있습니다. 아무것도 다운로드 받고 싶지 않다면 CDN을 통한 URL로 라이브러리를 사용해도 됩니다.

사용 방법

기본적인 사용 구문은 다음과 같습니다.

proj4(fromProjection[, toProjection, coordinates])

인자 fromProjection와 toProjection는 proj이거나 WTK 문자열일 수 있습니다. coordinates 인자는 {x:x,y:y}와 같은 객체 형태이거나 [x,y]와 같은 배열 형태일 수 있습니다.

fromProjection, toProjection, coordinates 인자가 모두 지정되면, 지정된 좌표가 fromProjection 좌표 체계에서 toProjection 좌표 체계로 변환된 결과를 반환합니다. 변환된 결과는 지정된 좌표 인자의 형태와 같습니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var secondProjection = "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

var result = proj4(firstProjection, secondProjection, [2, 5]);
// [-2690666.2977344505, 3662659.885459918]

만약 1개의 projection 만을 사용한다면 해당 인자는 fromProjection을 의미하며, firstProjection은 WGS84 경위도 좌표계가 됩니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var result = proj4(firstProjection, [-71, 41]);
// [242075.00535055372, 750123.32090043]

또한 coordinates 인자 없이 projection 인자만을 사용한다면, forward와 inverse 매서드를 갖는 객체가 반환되는데, forward는 fromProjection 좌표계에서 toProjection로의 좌표 변환을, inverse는 toProjection 좌표계에서 fromProjection로의 좌표 변환을 수행하는 매서드입니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var secondProjection = "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

var resultA = proj4(firstProjection, secondProjection).forward([2, 5]);
// [-2690666.2977344505, 3662659.885459918]

var resultB = proj4(secondProjection,firstProjection).inverse([2,5]);
// [-2690666.2977344505, 3662659.885459918]

projection 인자가 하나만 지정되면, 지정된 인자는 toProjection에 해당되며 fromProjection은 WGS84 경위도 좌표계가 됩니다.

이름을 가지는 투영변환

만약 문자열로 투영변환에 이름을 부여하고, 이 이름으로 좌표 변환을 수행하고자 한다면 proj4.defs 매서드를 사용할 수 있습니다.

proj4.defs('WGS84', "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees");

또는 아래처럼 배열로 여러개의 투영변환을 정의할 수 있습니다.

proj4.defs([
  [
    'EPSG:4326',
    'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]]'
  ],
  [
    'EPSG:4269',
    '+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
  ]
]);

그러면, 언제라도 다음처럼 사용할 수 있습니다.

var p = proj4('EPSG:4326', 'EPSG:4269');
var result = p.forward([-71, 41]);
// [-2690599.9886444192, 3662814.7663661353]

사실 위 코드의 첫번째 라인은 다음의 축약형입니다.

var p = proj4(proj4.defs('EPSG:4326'), proj4.defs('EPSG:4269'));

미리 정의된 이름을 가지는 투영변환은 EPSG:4326, EPSG:4269, EPSG:3857입니다. 아울러 EPSG:4326은 WGS84라는 이름으로도 정의되어 있으며, EPSG:3857은 EPSG:3758, GOOGLE, EPSG:900913, EPSG:102113이라는 다양한 이름으로도 정의되어 있습니다. 이에 대한 proj4js 라이브러리의 코드를 살펴보면 다음과 같습니다.

defs('EPSG:4326', "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees");
defs('EPSG:4269', "+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees");
defs('EPSG:3857', "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs");

defs.WGS84 = defs['EPSG:4326'];
defs['EPSG:3785'] = defs['EPSG:3857']; // maintain backward compat, official code is 3857
defs.GOOGLE = defs['EPSG:3857'];
defs['EPSG:900913'] = defs['EPSG:3857'];
defs['EPSG:102113'] = defs['EPSG:3857'];

아래는 한국의 좌표계를 예로 들어 proj4js의 설명을 담은 글입니다. 2013년도 글이라 현재 버전의 API와 맞지 않을 수 있습니다.

[GIS] 오픈소스, 자바스크립트 좌표계 변환 라이브러리, proj4js

끝으로 EPSG 코드를 통한 proj 및 wkt를 얻을 수 있는 사이트에 대한 글은 아래와 같습니다.

EPSG.io를 통한 proj4 문자열 얻기