파이썬 아나콘다와 주피터 랩

파이썬을 개발 환경을 준비하는 방법 중에 가장 대중적인 것은 아나콘다를 활용하는 법입니다. 아나콘다는 패키지 관리와 디플로이를 효율적으로 할 수 있도록 하는 파이썬과 프로그래밍 언어의 자유-오픈 소스 배포판입니다. 개발에 사용하는 패키지 버전들은 패키지 관리 시스템 conda를 통해 관리됩니다.

예를 들어 내 컴퓨터에서 개발을 위해 여러 개발환경을 구축한다고 생각해봅니다. 때로 RNN 프로젝트나 Django를 활용한 웹개발, CNN을 활용한 Classification 문제를 해결한다고 생각해보면 각 프로젝트마다 python의 버전이나 활용하는 패키지가 있을 것입니다.

또 다른 사람이 만들었던 개발환경을 내 컴퓨터에 그대로 복사하고 싶을 때가 있습니다. 이미 내 컴퓨터에 있는 환경과 같다면 크게 문제될 것이 없겠지만 다른 것이 많이 있다면 좀 난감한 상황이겠죠.

아나콘다는 이러한 환경에 독립적인 프로젝트를 만들 수 있도록 도와줍니다. 이러한 툴이 없다면 개발 환경마다 python 버전과 의존성 있는 패키지들을 수정해야 하는 번거로운 작업들을 해야합니다. 그만큼 개발하는데 많은 시간이 소요되겠죠.

아나콘다 홈페이지에는 해당 제품들을 소개하며 모든 데이터 사이언티스트를 위한 도구라고 소개하고 있습니다. 그만큼 사용하기 쉽고 범용적이고 대중적인 도구라는 의미가 되겠습니다.

아나콘다는 Window, Mac, Linux를 모두 지원하니 사용하시는 OS에 맞춰서 다운로드 받으시면 되겠습니다. 저의 경우에는 Mac을 사용하기 때문에 Mac용 아나콘다를 다운로드 받아서 사용하고 있습니다.

다운 받은 후에 실행하면 아래와 같은 형태의 화면이 표시됩니다. 이중에서 이번 파이썬 코드 리뷰에 사용하는 도구는 Jupyter Lab이기 때문에 간단히 살펴보겠습니다. 사실 Jupyter Lab은 Jupyter Notebook의 확장판이라고 생각하시면 되기 때문에 이전에 Jupyter Notebook을 사용해 보신 분이라면 쉽게 사용하실 수 있습니다. 사용 방식이나 UI 구성은 거의 같습니다.
이 외에도 VS Code라는 훌륭한 개발도구도 있으니 사용해보시는 것도 추천합니다.

Jupyter Lab의 가장 좋은 장점은 Interactive하고 Reproducible한 특징이라고 할 수 있습니다. 마땅히 우리말로 설명하기가 어렵기 때문에 Jupyter Lab에서 소개하는 단어를 그대로 사용하겠습니다.
이러한 특징이 있기 때문에 사용자는 코딩 하고 바로 그 결과를 확인 할 수 있습니다. 무엇보다 무료이고 가볍고 웹 기반의 형식으로 되어 있기 때문에 UI도 따로 설명할 것이 없을 정도로 간단합니다.

그러나 그 기능과 활용도는 매우 강력하다고 할 수 있습니다. 아래의 이미지는 Jupyter Lab 공식 홈페이지에서 제공하는 이미지입니다. 보시는 것처럼 풍성한 시각화 도구를 지원하고 있습니다. 자세한 내용은 아래의 링크로 접속하시면 확인할 수 있습니다.

https://jupyter.org/

이 외에도 Colab(Colaboratory) 이라는 강력한 도구가 있습니다.

줄여서 ‘Colab’이라고도 하는 Colaboratory를 사용하면 브라우저에서 Python을 작성하고 실행할 수 있습니다. 자세한 소개 영상은 아래의 영상을 확인하시기 바랍니다.

Colab을 사용하면서 얻을 수 있는 장점은 별도의 개발환경을 구축하지 않아도 된다는 점, GPU를 제한적이지만 무료로 사용할 수 있다는 점, 그리고 공유가 간편하다는 장점이 있습니다. 조금 더 좋은 환경을 사용하려면 유료 결제를 통해서 Colab Pro 버전을 활용할 수도 있습니다.

저도 Colab Pro 버전을 사용하고 있는데 확실히 Colab 무료 버전 보다는 조금 더 나은 테스트 환경을 제공해줍니다. (많이는 아니지만…)

Google Colab GPU Text-classification

Colaboratory(혹은 Colab)를 사용하면 브라우저에서 Python을 작성하고 실행할 수 있습니다. 장점이라면 별도의 구성이 필요 없고 무료로 GPU를 사용할 수 있다는 장점이 있습니다. 또 만든 코드를 간단하게 공유할 수도 있습니다.

감성분석(Text Classification)에 사용한 데이터는 네이버에서 공개한 영화 평점 정보입니다. 해당 데이터는 아래 링크에서 받을 수 있습니다.
https://github.com/e9t/nsmc

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import io

본 예제에서 사용할 konlpy를 Colab에 설치합니다.

!pip install konlpy
from konlpy.tag import Okt
okt = Okt()

Colab에서 사용할 파일을 사용자 계정의 구글 드라이브에 업로드합니다. 그리고 업로드한 파일 정보를 Colab에서 읽을 수 있도록 필요한 python 라이브러리를 등록해야합니다. 아래 코드를 실행하면 구글 계정에 인증할 수 있는 정보가 나오고 키값을 입력하면 드라이브에 접근할 수 있습니다.

from google.colab import drive
drive.mount('/content/gdrive')

구글 드라이브에 접근이 완료되면 파일이 있는 디렉토리 위치로 이동합니다.

from google.colab import drive
drive.mount('/content/gdrive')
%cd gdrive/My\ Drive/Colab\ Notebooks

해당 위치로 이동한 후에 %ls 명령을 실행시켜보면 해당 폴더에 있는 파일 리스트를 표시해줍니다. 파일 중에서 학습에 사용할 파일을 open하면 됩니다.

%ls
def read_data(filename):
    with io.open(filename, 'r',encoding='utf-8') as f:
        data = [line for line in f.read().splitlines()]
        data = data[1:]
    return data 

sentences = []
# 테스트를 위해 길이가 30 이하인 문장을 읽음
for sentence in read_data('./ratings_test.txt'):
  if len(sentence) <= 30:
    sentences.append(sentence)

해당 파일을 읽어보면 아래와 같은 형태로 데이터가 구성되어 있습니다. 이전 예제에서 설명했던 것처럼 1은 긍정적인 답변을 0은 부정적인 답변을 의미합니다.

['6270596|굳 ㅋ|1',
 '7898805|음악이 주가 된, 최고의 음악영화|1',
 '6315043|진정한 쓰레기|0',
 '7462111|괜찮네요오랜만포켓몬스터잼밌어요|1',
 '10268521|소위 ㅈ문가라는 평점은 뭐냐?|1' ...
class Vocab():
    def __init__(self):
        self.vocab2index = {'<pad>':0,'<unk>':1} # padding 0, unkown 1
        self.index2vocab = {0:'<pad>',1:'<unk>'} # 0 padding, 1 unkown
        self.vocab_count = {}
        self.n_vocab = len(self.vocab2index)
        
    def add_vocab(self, sentence):
        for word in sentence:
            if word not in self.vocab2index:
                self.vocab2index[word] = self.n_vocab
                self.index2vocab[self.n_vocab] = word
                self.vocab_count[word] = 1
                self.n_vocab += 1
            else:
                self.vocab_count[word] += 1

vo = Vocab()

def charStrip(s):
    s = s.replace('"','').replace('「','').replace('」','').replace('“','').replace('?','').replace('”','')
    s = s.replace('(',' ').replace(')',' ').replace('‘','').replace('’','').replace('□','').replace('◆','').replace('◇','')
    s = s.replace('[',' ').replace(']',' ').replace('○','').replace('△','').replace('◎','').replace('▣','').replace('◇','')
    s = s.replace('.',' ').replace('*',' ').replace('.',' ').replace('~',' ')
    
    return s

x = [] # text sentence
y = [] # label
for sentence in sentences:
    arr = sentence.split('|')
    if(len(arr) == 3):
      sentence = okt.morphs(charStrip(arr[1]))
      
      vo.add_vocab(sentence)
      x.append(sentence) 
      y.append(float(arr[2])) 
MAX_SEQUENCE_LENGTH = 0

for sentence in x:
    if MAX_SEQUENCE_LENGTH < len(sentence): MAX_SEQUENCE_LENGTH = len(sentence)

MAX_SEQUENCE_LENGTH

데이터 중에서 가장 긴 문장을 확인해봅니다. 이 문장의 크기가 Sequence Length가 됩니다. 이 문장의 길이보다 작은 문장의 경우 빈칸은 <unk> 값으로 채워줍니다.

def tensorize(vocab, sentence):
    idx = [vocab.vocab2index[word] for word in sentence]
    #return torch.Tensor(idx).long().item()
    return idx

tmp_tensor = []
for sentence in x:
    tmp = tensorize(vo, sentence)
    tmp_zero = np.zeros(MAX_SEQUENCE_LENGTH)
    
    for i,val in enumerate(tmp):
        tmp_zero[i] = val
        
    tmp_tensor.append(tmp_zero)
    
x_data = torch.Tensor(tmp_tensor).long()
y_data = torch.Tensor([float(t) for t in y])
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
output : device(type='cuda')

구글 Colab에서는 GPU를 사용할 수 있기 때문에 필요한 설정을 해줍니다. CPU로 연산할 때보다 훨씬 빠른 계산속도를 보여줍니다.

학습용 데이터는 8/2로 학습용 Train Data / Valid Data 데이터로 나눠줍니다.

DATA_LENGTH = x_data.size(0)

train_cnt = int(DATA_LENGTH*.8)
valid_cnt = DATA_LENGTH - train_cnt
print('train_cnt, valid_cnt = ',train_cnt,valid_cnt)

idx = torch.randperm(DATA_LENGTH)
x_train = torch.index_select(x_data, dim=0, index=idx).to(device).split([train_cnt, valid_cnt], dim=0)
y_train = torch.index_select(y_data, dim=0, index=idx).to(device).split([train_cnt, valid_cnt], dim=0)

각 데이터셋은 pytorch의 Dataset, DataLoader를 사용해서 배치사이즈로 나눠줍니다. 학습할 데이터가 많은 경우에 많은 데이터를 한번에 읽으면 메모리 부족현상이 발생하는데 Dataset을 배치사이즈로 분리해서 로딩하면 메모리를 적게 사용하게 되어 큰 데이터도 학습할 수 있습니다.

from torch.utils.data import Dataset, DataLoader

class SimpanDataset(Dataset):
    
    def __init__(self, data, label):
        super().__init__()
        self.data = data
        self.labels = label
        
    def __len__(self):
        return len(self.data)
    
    def __getitem__(self, idx):
        return self.data[idx], self.labels[idx]
    
# train_loader
train_loader = DataLoader(dataset=SimpanDataset(x_train[0], y_train[0]), batch_size=250, shuffle=True)
valid_loader = DataLoader(dataset=SimpanDataset(x_train[1], y_train[1]), batch_size=250, shuffle=False)

학습용 모델을 아래와 같이 설정합니다. 학습 모델은 Embedding -> BiLSTM -> Softmax 레이어를 통과하면서 최종 output을 만들어냅니다.
단, output은 모든 Sequence의 데이터를 사용하지 않고 마지막 시퀀스의 값만 사용하며 LSTM의 모델에서 bidirectional을 True로 설정했기 때문에 output*2의 값이 리턴됩니다. 학습에 최종 결과물은 0과 1이기 때문에 hidden_size는 2입니다.

class SimpanClassificationModel(nn.Module):
    
    def __init__(self, input_size, hidden_size):
        super().__init__()
        
        self.embedding = nn.Embedding(input_size, 300)
        
        self.rnn = nn.LSTM(input_size=300, hidden_size=100, num_layers=4, batch_first=True, bidirectional=True)
        
        self.layers = nn.Sequential(
            nn.ReLU(),
            nn.Linear(100*2,100),
            nn.Linear(100,30),
            
            nn.Linear(30, hidden_size),
        )
        
        self.softmax = nn.Softmax(dim=-1)
        
    def forward(self, x):
        y = self.embedding(x)
        y,_ = self.rnn(y)
        y = self.layers(y)
        
        return self.softmax(y[:,-1,:])
    
input_size = vo.n_vocab
hidden_size = torch.unique(y_train[1]).size(dim=-1)

model = SimpanClassificationModel(input_size, hidden_size)
model = model.cuda()
# loss & optimizer setting
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters())

hist_loss = []
hist_accr = []

epochs = 501
# start training
model.train()

for epoch in range(epochs):
    epoch_loss = 0
    for x,y in train_loader:
      x, y = x.to(device), y.to(device)
      output = model(x)
      loss = criterion(output, y.long())
        
      optimizer.zero_grad()
      loss.backward()
      optimizer.step()
        
      epoch_loss += loss.item()
      accuracy = (torch.argmax(output, dim=-1) == y).float().mean().item()
    
      
    hist_loss.append(epoch_loss)
    hist_accr.append(accuracy)
        
    print('Cost: {:.6f}, Accuracy : {:.6f}'.format(loss.item(),accuracy))
    print('--{}--'.format(epoch))

학습의 진행상황을 기록하기 위해서 2개의 배열(hist_loss, hist_accr)을 사용합니다.

hist_loss는 loss값의 변화를 기록하는 배열이며 hist_accr은 해당 모델의 정확도 정보를 얻기 위해서 만든 배열입니다. 학습이 진행되며 해당 배열에 데이터가 기록되고 matplotlib.pyplot을 사용해서 그래프를 그려봅니다.

import matplotlib.pyplot as plt

fig, ax = plt.subplots(2,1)
fig.set_size_inches((12, 8)) 

ax[0].set_title('Loss')
ax[0].plot(hist_loss, color='red')
ax[0].set_ylabel('Loss')
ax[1].set_title('Accuracy')
ax[1].plot(hist_accr, color='blue')
ax[1].set_ylabel('Accuracy')
ax[1].set_xlabel('Epochs')

plt.show()
model.eval()
    
for xv, yv in valid_loader:
    output = model(x)
    
    accuracy = (torch.argmax(output, dim=-1) == y).float().mean().item()

    hist_loss.append(loss.item())
    hist_accr.append(accuracy)

    print('Accuracy : {:.6f}'.format(accuracy))

모델의 학습이 완료된 후 valid data를 통해서 학습 모델의 정확도를 알아봅니다.

Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667
Accuracy : 0.916667