본문 바로가기
Book Review/[파이토치 딥러닝 마스터] 리뷰

Data to Tensor: 이미지, 테이블, 시계열 , 텍스트 데이터를 텐서로

by 3n952 2023. 4. 5.

지난 포스팅에서 텐서는 파이토치 데이터의 하나의 블록으로써 구조를 만들어내는 단위와 같다는 것을 배웠습니다.
실제로 딥러닝 모델이 입력으로 받는 것도, 출력으로 내보내는 것도 모두 텐서입니다.
학습 과정에서 갱신되는 파라미터들도 모두 텐서의 형태입니다.
따라서 파이토치의 딥러닝의 핵심은 데이터를 어떻게 텐서로 변환하는지 그 과정을 아는 것입니다.

이번 포스팅에서는 다양한 데이터 형태를 텐서로 표현하는 방법에 대해 알아보도록 하겠습니다.
각 주제에 대해 간단한 예시 데이터로 알아볼 예정입니다.

 

1. 이미지 데이터

컴퓨터 비전 분야에서 이미지를 다루는 컨볼루션 네트워크(CNN)는 큰 혁명을 불러왔습니다.

이를 가능하게 하기 위해서는 이미지 포맷을 읽어 파이토치의 딥러닝 모델이 기대하는 방식에 맞춰 이미지의 다양한 부분을 표현하는

텐서로 표현 가능해야 합니다.

일반적으로 이미지는 3개의 차원을 가진 텐서로 표현됩니다. (채널 수, 높이, 너비)

흑백 채널을 가지면 채널 수는 1개, 컬러 채널(RGB)을 가지면 채널 수는 3개인 것입니다. 

 

-이미지 파일 로딩 

실제로 이미지 파일을 로드해서 텐서로 다루는 과정을 코드로 살펴보겠습니다.

데이터는 https://github.com/deep-learning-with-pytorch/dlwpt-code/tree/master/data/p1ch4/image-dog에서 가져왔으며 강아지 사진입니다.

먼저 이미지 파일을 로드해 보겠습니다.

파일을 다운로드한 경로를 imageio.imread메서드로 불러오면 됩니다.

import imageio.v2

img_arr = imageio.v2.imread('파일 경로')
img_arr.shape

#out : (720, 1280, 3)

이미지 파일을 불러오면 (720, 1280, 3)의 사이즈의 넘파이 배열을 가지고 있습니다.

넘파이 배열을 넘겨주는 라이브러리는 언제든 텐서로 바꿔 표현할 수 있습니다.

이미지 데이터를 다루는 순서는 (채널, 높이, 너비)이므로 이 순서를 지켜줘야 합니다.

차원의 순서를 바꿔주기 위해 레이아웃을 변경해 줍니다.

앞에서 얻은 (높이, 너비, 채널)의 레이아웃을 (채널, 높이, 너비)로 바꿔보는 것입니다.

이를 위해 permute메서드를 사용해 줍니다.

img = torch.from_numpy(img_arr)
out = img.permute(2, 0 ,1)

여기까지가 하나의 이미지를 다뤄 텐서로 표현한 것이다.

그렇다면 다수의 이미지 데이터 셋을 텐서로 표현하려면 어떻게 해야 할까?

stack으로 공간을 할당 한 뒤 디렉터리에서 읽은 이미지를 불러오고 여러 개의 이미지를 배치로 두면 간단하게 텐서로 표현될 것입니다!

batch_size = 3
batch = torch.zeros(batch_size, 3, 256, 256, dtype = torch.uint8)

import os

data_dir = '이미지 파일의 경로'
filenames = [name for name in os.listdir(data_dir)
             if os.path.splitext(name)[-1] == '.png']
for i, filename in enumerate(filenames):
    img_arr = imageio.imread(os.path.join(data_dir, filename))
    img_t = torch.from_numpy(img_arr)
    img_t = img_t.permute(2, 0, 1)
    img_t = img_t[:3] 
    batch[i] = img_t

batch_size는 이미지 데이터에 맞게 설정해 주시면 됩니다. 

이후 이미지 데이터 여러 개를 불러 (배치 크기, 채널 수, 높이, 너비) 사이즈의 텐서로 바꿀 수 있습니다.

높이 256, 너비 256, 채널 3을 가지는 배치를 만들기 위해 입력 디렉터리에서 이미지를 읽어 텐서로 저장한 코드입니다.

-데이터 정규화

신경망은 일반적으로 부동 소수점 텐서를 입력으로 사용합니다. 입력값이 0~1 사이이거나 -1 ~ 1 사이를 가질 때 훈련 성능이

가장 뛰어나다고 합니다. 따라서 대부분의 경우 부동 소수점으로 캐스팅하고 픽셀 값을 정규화하는 과정을 거쳐야 합니다.

관례적으로 가장 단순하게는 각 픽셀 값에 255를 나눠 0~1사이의 값을 가지게 할 수 있다.

batch = batch.float()
batch /= 255.0

다른 정규화 방법은 입력 데이터의 평균과 표준 편차를 활용하는 것이다.

채널을 기준으로 그 값들이 표준편차를 넘지 않게 만들기도 합니다.

n_channels = batch.shape[1]
for c in range(n_channels):
    mean = torch.mean(batch[:, c])
    std = torch.std(batch[:, c])
    batch[:, c] = (batch[:, c] - mean) / std

 

정규화 외에도 추가적인 작업이 있지만 이는 추후에 다뤄보도록 하겠습니다.

 

2. 테이블 데이터

테이블 데이터는 csv파일이나 스프레드시트와 같이 샘플 별로 한 행, 샘플에 대한 정보별로 한 열을 이루는 단순한 데이터 형태입니다.

테이블 내 데이터 샘플의 속성들은 데이터 타입이 다를 수 있고 독립적일 수 있습니다.(열마다 다 다르다.)

따라서 이를 올바르게 처리하여 우리가 찾는 값에 적용할 필요가 있습니다.

 

실제 데이터 셋을 가지고 상이한 데이터 형태를 가지는 테이블을 텐서로 만들어 신경망에 넣을 수 있도록 해보겠습니다.

데이터 셋은 와인 샘플의 화학적 특성이 기록된 데이터입니다.

https://github.com/deep-learning-with-pytorch/dlwpt-code/tree/master/data/p1ch4/tabular-wine

파일을 열면 화학적 성분과 맛에 대한 점수가 기록되어 있음을 알 수 있습니다.

 

-데이터를 텐서로 읽어오기

파이썬에는 csv파일을 읽을 수 있는 방법이 크게 세 가지가 존재합니다.

1. 파이썬 내장 csv모듈 사용

2. 넘파이 사용

3. 판다스 사용

이 중 저희는 텐서로 변환하기에 호환성이 좋은 넘파이를 사용해 보도록 하겠습니다.

import numpy as np
import torch

import csv
wine_path = "csv 파일 경로"
wineq_numpy = np.loadtxt(wine_path, dtype=np.float32, delimiter=";",
                         skiprows=1)
wineq_numpy

32비트 부동 소수점의 2차원 넘파이 배열이 만들어졌습니다.

이제 이를 텐서로 바꾸고 사이즈를 확인해 보겠습니다.

wineq = torch.from_numpy(wineq_numpy)

wineq.shape
#out : tensor([4898, 12])

12개의 특성을 가진 샘플이 4898개가 있다는 것 정보를 텐서로 표현한 것입니다!

 

각 열에 대한 데이터들이 각기 다른 타입(정수, 문자열 등등)을 가지고 있고 이를 숫자로 표현해야만 합니다.

가령 파랑,빨강과 같은 색상 feature에 값들은 파랑 = 0, 빨강 = 1과 같은 정수값으로 나타날 수 있습니다.

이때 중요한 것은 데이터를 파악하는 것입니다.

데이터의 형태에는 크게 세 가지가 존재합니다.

1. 연속값 데이터, 2. 순서값 데이터, 3. 카테고리값 데이터

 

연속값 데이터는 그대로 사용하면 되며, 카테고리값 데이터는 범주를 나눠 임의의 숫자로 표현하면 됩니다.

다만 순서값 데이터인 경우에는 연속값 데이터, 카테고리값 데이터처럼 쓰일 수 있습니다.

따라서 그 데이터의 의미와 형태를 잘 파악하여 구분해 주는 것이 중요합니다.

 

여기서는 맛의 점수를 카테고리값 데이터로 분류하여 텐서로 표현하는 방법에 대해 알아보겠습니다.

-정수 벡터로 레이블화

정수 벡터로 레이블화 하기 위해서는 마지막 열의 값들을 모두 불러와야 합니다.(마지막 열이 맛의 점수가 들어 있기 때문에)

기존의 맛의 점수가 어떻게 되어있는지 확인하기 위해 텐서로 불러와 확인해 보도록 하겠습니다.

#wineq의 마지막 열의 모든 행으로 슬라이싱
target = wineq[:, -1]
target 

#out: tensor([6., 6., ...., 7., 6.])

출력된 값을 보면 부동 소수점으로 이뤄진 텐서를 확인할 수 있습니다.

정수 벡터로 전치해 보도록 하겠습니다.

target = wineq[:, -1].long()
target

#out = tensor([6,6,...,7,6])

맛 점수뿐만 아니라 와인 색과 같이 문자열로 이뤄진 레이블을 문자열마다 대응하는 정수로 할당해서 처리하면 됩니다!

 

- 원핫 인코딩

카테고리 값의 범위 내에서 벡터 안에서 각각의 원소에 대응하도록 정해서 원소 하나만 1로 설정하고

나머지는 0으로 처리하는 것을 원핫 인코딩이라고 합니다.

예를 들어 값 1은 벡터(1,0,0,0 ...)로 처리하고 값 5는 (0,0,0,0,1,0,0 ...)로 처리하는 식입니다.

원핫 인코딩은 scatter_메소드를 이용하여 소스 텐서와 함께 전달된 인자의 인덱스를 따라 새 텐서를 채워주면서 

이뤄집니다.

target_onehot = torch.zeros(target.shape[0], 10)
target_onehot.scatter_(1, target.unsqeeze(1), 1.0)

위 코드에서 scatter_메소드는 새로운 텐서를 반환하는 것이 아닌 바꿔치기하는 메소드이다.

해당 메소드의 인자는 다음과 같은 의미를 가집니다.

  • 뒤에 오는 두 개의 인자가 따라야 하는 차원을 명시
  • 원핫으로 인코딩할 요소를 가르키는 인덱스가 들어있는 텐서
  • 원핫 인코딩할 원소가 들어있는 텐서 혹은 단일 스칼라

두 번째 인자를 살펴보면 인덱스 텐서는 텐서를 원핫 인코딩할 때 동일한 차원 수를 만들기 위해 필요합니다.

target_onehot이 2차원(4898 * 10)이므로 unsqueeze를 통해 target에 추가 차원을 만들어 준 것입니다.

 

위의 정수 벡터와 원한 인코딩 방식에는 차이점이 있습니다.

점수상에 순서가 있고 점수 간의 거리가 있는 경우는 정수 벡터를 사용하기 편리합니다.

하지만 점수가 이산적인 경우에 값 사이의 순서나 거리 개념이 없기 때문에 원핫 인코딩이 더 낫습니다.

 

3. 시계열 데이터

 일반적으로 시계열 데이터는 일정한 시간 단위 동안 수집된 일련의 순차적으로 정해진 데이터를 의미합니다.

즉, 시간별로 구성된 값의 집합이라고 보시면 됩니다.

간단하게 주식차트를 생각하면 됩니다. 시간 단위 (1분, 5분, 10분)로 주식 가격을 값으로 가진다면 시계열 데이터라고 할 수 있습니다.

 

시계열 데이터 셋에서 각 행은 연속적인 시간상에서의 한 지점의 값을 의미합니다. 따라서 시간적 순서가 있는 데이터를 분석할 때,

이전 시간의 상황을 고려하여 현재 시간대의 상황을 분석하는 것이 필요합니다.

대부분의 시계열 데이터는 크기가 C인 N개의 병렬 시퀀스로 표현이 가능합니다.

여기서 C는 채널을 나타내는데, 1차원 데이터를 위한 열(column)과 동일합니다.

N차원은 시간 축을 나타내며 한 차원당 한 시간 단위를 의미합니다.

 

그렇다면 시계열 데이터를 불러와 확인해 보고 이를 신경망 모델에 훈련하기 전까지의 과정을 한번 살펴보겠습니다.

데이터는 깃허브에 있는 bike sharing 데이터로 진행하였습니다.

https://github.com/deep-learning-with-pytorch/dlwpt-code/tree/master/data/p1ch4/bike-sharing-dataset

#깃 클론 주소
!git clone https://github.com/deep-learning-with-pytorch/dlwpt-code.git

데이터를 읽어보겠습니다.

import numpy as np
import torch


bikes_numpy = np.loadtxt(
    "#저장된 디렉토리 (~~/~~/~~/hour-fixed.csv의 형식)", 
    dtype=np.float32, 
    delimiter=",", 
    skiprows=1, 
    converters={1: lambda x: float(x[8:10])}) 
bikes = torch.from_numpy(bikes_numpy)
bikes

-시간 단위로 데이터 만들기

길이가 L인 C개의 시퀀스를 가지는 N개의 샘플을 얻어보도록 하겠습니다.

즉, 3차원 텐서인 N x C x L로 만들어지는 것입니다.

C에는 각 시간대별 데이터 정보가 들어가야 하므로 17개

(레코드 인덱스, 일자, 계절, 연도, 월, 시간...., 대여 자전거 수 -> 총 17개),

L은 1시간으로 하루 단위이므로 24가 됩니다.

 

처음 데이터셋 텐서의 차원 정보를 보고 나면 이해하기 쉽습니다.

bike.shape

#out: torch.Size([17520, 17])

17520시간에 대해 17개 정보의 값을 가진다는 것을 알 수 있습니다.

이제 이 데이터를 일자, 시간, 17개의 세 개의 축으로 만들어보겠습니다.

daily_bikes = bikes.view(-1, 24, bikes.shape[1])
daily_bikes.shape

#out: torch.Size([730, 24, 17])

view를 호출하여 저장 공간에 있는 데이터를 바꾸지 않고 차원수나 스트라이드 정보를 바꿔 텐서를 다르게 구성했습니다.

-1은 할당하고 남은 차원을 자동으로 처리하는 placeholder로 사용됩니다.

즉 24, 17로 할당하고 남은 요소를 차원에 배당하는 것입니다.

 

훈련 준비를 위해 여기서는 N x C x L의 순서로 전치하여 텐서를 구성해 보겠습니다.

daily_bikes = daily_bikes.transpose(1, 2)

날씨상태에 대한 값을 카테고리로 분류하여 원핫 인코딩 벡터로 만든 후 원래 데이터 셋에 합병해 보겠습니다.

가볍게 첫날에 대한 데이터로 확인해 보도록 하겠습니다.

first_day = bikes[:24].long()
weather_onehot = torch.zeros(first_day.shape[0], 4)
first_day[:,9]

#out: tensor([1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2])

하루 시간대만큼 행을 가져와 first_day변수에 넣고

사이즈에 맞게 0으로 채워진 행렬을 초기화합니다. 

실제로 하루 시간대만큼 가져온 데이터의 날씨 상태에 대한 정보(9번째 인덱스)를 가져오면 

저렇게 반환하는 것을 확인할 수 있습니다.

 

사이즈에 맞게 0으로 채워진 텐서는 왜 만들었을까요?

바로 원핫 인코딩 벡터를 만들기 위해서입니다.

이전과 마찬가지로 scatter_메소드를 사용하겠습니다.

weather_onehot.scatter_(
    dim=1, 
    index=first_day[:,9].unsqueeze(1).long() - 1, 
    value=1.0)

''' out: tensor([[1., 0., 0., 0.],
                 [1., 0., 0., 0.],
                  ...,
                 [0., 1., 0., 0.],
                 [0., 1., 0., 0.]]) '''

 

원핫 인코딩 벡터를 원래의 데이터셋에 병합하려면 cat함수를 사용하면 됩니다.

torch.cat((bikes[:24], weather_onehot), 1)[:1]

'''out: tensor([[ 1.0000,  1.0000,  1.0000,  0.0000,  1.0000,  0.0000,  0.0000,
          6.0000,  0.0000,  1.0000,  0.2400,  0.2879,  0.8100,  0.0000,
          3.0000, 13.0000, 16.0000,  1.0000,  0.0000,  0.0000,  0.0000]]) '''

첫날에 대한 데이터 텐서(bikes[:24])와 원핫 인코딩한 벡터 텐서(weather_onehot)를 1차원으로 합쳐 그 값을 살펴본 것을 의미합니다.

(17개의 값들 + 원핫 인코딩한 4개의 값 => 21개의 요소)

 

4. 텍스트 데이터

텍스트 표현은 자연어 처리(NLP) 분야에서 큰 주목을 받았습니다.

모델의 이전 출력과 현재 입력을 섞어 재귀적으로 반복하는 형태의 모델을 대표적으로 사용합니다.

이러한 모델을 순환신경망 모델(RNN: recurrent neural network)라고 합니다.

 

텍스트 데이터는 RNN의 신경망이 처리할 수 있도록 숫자의 텐서로 바꿔야 합니다.

1. 단어 단위로 개별 단어를 처리

2. 문자 단위로 문자를 처리

 

가령, apple is delicious가 있으면 1번의 경우는 apple, is, delicious를 처리하는 것이고

2번의 경우는 a, p, p, l, e, i, s, d, e, l, i, c, i, o, u, s를 처리하는 것이다,

 

문자 단위를 처리하는 경우에도 문자를 원핫 인코딩해주는 방식을 통해 텐서로 바꿀 수 있다.

ASCII코드와 같이 정수로 인코딩하는 것과 일맥상통한다.

따라서 문자의 종류 수가 벡터의 크기가 되어 각 문자가 이 크기의 벡터로 표현됩니다.

코드로 데이터를 불러와서 문자 단위 원핫 인코딩을 진행해 보겠습니다.

데이터는 구텐베르크 웹사이트의 '오만과 편견' 중 한 부분에 대한 텍스트 데이터입니다.

import numpy as np
import torch

with open('path/to/your/directory/~.txt', encoding='utf8') as f:
    text = f.read()
    
#행으로 나누고 201번째 행의 텍스트 가져오기
lines = text.split('\n')
line = lines[200]

#ASCII코드와 같이 하드코딩
letter_t = torch.zeros(len(line), 128)

#문자 하나하나를 표현하기 위해 웟핫 인코딩
for i, letter in enumerate(line.lower().strip()):
    letter_index = ord(letter) if ord(letter) < 128 else 0  # <1>
    letter_t[i][letter_index] = 1

마찬가지로 단어 단위 원핫 인코딩할 수 도 있습니다.

 

하지만 문자나 단어 단위의 인코딩을 진행하면 중복된 단어를 제거하고 철자를 대체해서 가짓수를 줄여야 하며 시제에 대해 구분하지 않는 것도 필요합니다. 새로운 단어가 등장하면 벡터에 열을 추가해서 모델이 새 항목에 대해 가중치를 조정하도록 해주어야 합니다.

따라서 인코딩 크기를 적절하게 줄이고 늘어나지 않게 하는 방법이 필요합니다.

이때 등장하는 개념이 임베딩(embedding) 기법입니다.

하나만 1 나머지가 0인 벡터를 만드는 대신 부동소수점 수를 가지는 벡터를 사용하는 것입니다.

임베딩을 통해 단어의 사분면에 표현하면 비슷한 단어끼리 군집하고 일관된 공간 관계를 유지할 수 있습니다.

단어의 임베딩 벡터를 찾아 다른 단어를 더하거나 빼면 관련된 비유 관계를 찾을 수 있습니다.

가령, 사과 - 붉은 - 달콤함 + 노란 + 새콤함은 레몬 벡터 값과 상당히 유사해질 것입니다.

 

 

이번 포스팅에서는 다양한 데이터 종류와 그것을 텐서로 변환하거나 다루는 법에 대해 알아보았습니다.