VAE 기반 신약 분자 생성 #2 데이터 로드 및 토큰화 #
#2026-02-28
#1 MUV 데이터셋 가져오기
import deepchem as dc
tasks, datasets, transformers = dc.molnet.load_muv()
train_dataset, valid_dataset, test_dataset = datasets
train_smiles = train_dataset.ids
# → ['CCO', 'CC(=O)Oc1ccccc1C(=O)O', 'Cn1cnc2c1c(=O)n(c(=O)n2C)C', ...]
DeepChem 라이브러리로 MUV 데이터셋을 자동으로 다운로드해준다. MUV는 Maximum Unbiased Validation의 약자로, 신약 후보 물질을 가상으로 스크리닝하는 벤치마크 데이터셋이다. 수만 개의 분자가 SMILES 문자열 형태로 들어 있다.
DNA 데이터에서는 .ids는 샘플의 식별자였는데 chr22:20208963-20209064 같은 게놈 좌표가 ID였다. 그런데 분자 데이터에서는 SMILES 문자열 자체가 ID다. 왜냐하면 SMILES가 곧 분자의 고유한 이름이기 때문이다. CCO라고 쓰면 에탄올이고, 다른 어떤 분자도 CCO가 아니다. 그래서 train_dataset.ids를 가져오면 곧바로 학습용 SMILES 문자열 목록을 얻게 된다.
load_muv()가 데이터를 훈련, 검증, 테스트 세 세트로 나눠서 돌려주는 건 이전 챕터들과 동일한 패턴이다. 학습에는 훈련 세트를 쓰고, 하이퍼파라미터 조정에는 검증 세트를, 최종 성능 보고에는 테스트 세트를 쓴다.
#
#2 토큰화: 분자 문자열을 모델이 이해할 수 있는 조각으로 나누기
SMILES 문자열을 신경망에 넣으려면 먼저 문자열을 숫자로 바꿔야 한다. 그 첫 번째 단계가 토큰화다. 어떤 “조각"들이 이 데이터에 존재하는지를 파악하는 과정이다. 이 분석에서는 문자 수준 토큰화를 쓴다. SMILES 문자열을 한 글자 한 글자로 쪼개는 것이다. CC(=O)O라는 문자열은 C, C, (, =, O, ), O라는 일곱 개의 토큰으로 분해된다.
tokens = set()
for s in train_smiles:
tokens = tokens.union(set(s))
tokens = sorted(list(tokens))
max_length = max(len(s) for s in train_smiles)
빈 집합(set)을 하나 만들고, 모든 학습 SMILES를 하나씩 순회한다. 각 SMILES를 set()으로 감싸면 그 문자열에 등장하는 고유한 문자들의 집합이 나온다. CC(=O)O의 경우 {C, (, =, O, )}가 된다. 이걸 전체 토큰 집합에 union(합집합)으로 추가한다. 수만 개의 SMILES를 다 돌고 나면, 이 데이터셋에 등장하는 모든 고유한 문자가 하나의 집합에 모인다.
s = 'CC(=O)O'
set(s) = {'C', '(', '=', 'O', ')'}
이 집합에는 어떤 것들이 들어 있을까? 알파벳 대문자 C, N, O, S 같은 원소 기호가 있고, 소문자 c, n, o, s 같은 방향족 원소 기호가 있고, 괄호, 등호, 우물정자, 슬래시, 숫자 등 SMILES 문법에 쓰이는 특수 문자들이 있다. 이 모든 고유 문자가 모델의 “어휘(vocabulary)“가 된다. 모델이 아는 글자의 전체 목록인 셈이다.
tokens = ['#', '(', ')', '+', '-', '/', '0', '1', '2', ..., '=',
'B', 'Br', 'C', 'F', 'H', 'I', 'N', 'O', 'S', 'c', 'n',
'o', 's', '[', ']', '@', '\\', ...]
sorted()로 이 토큰들을 정렬하는 이유는 재현 가능성 때문이다. 파이썬의 집합(set)은 순서가 없어서 실행할 때마다 원소의 순서가 바뀔 수 있다. 정렬하면 항상 같은 순서가 보장되므로, 같은 코드를 다시 돌려도 같은 결과를 얻을 수 있다. 토큰의 순서가 중요한 이유는 나중에 각 토큰에 번호를 매겨서 원-핫 인코딩을 할 것이기 때문이다. 순서가 바뀌면 같은 문자에 다른 번호가 매겨져서 모델이 혼란에 빠진다.
#
#3 최대 길이: 왜 가장 긴 문자열의 길이를 알아야 하는가
max_length = max(len(s) for s in train_smiles)
# 예: 120 (가장 긴 SMILES 문자열의 길이)
마지막으로 모든 학습 SMILES 중 가장 긴 것의 길이를 구한다. 이건 신경망의 근본적인 제약 때문이다. 신경망은 고정된 크기의 입력을 기대한다. 배치 안의 모든 데이터가 같은 shape이어야 행렬 연산이 가능하다.
그런데 SMILES 문자열의 길이는 분자마다 다르다. 에탄올은 CCO로 3글자이고, 복잡한 약물 분자는 100글자가 넘을 수도 있다. 이걸 하나의 배치에 넣으려면 모든 문자열의 길이를 통일해야 한다. 가장 긴 문자열의 길이에 맞춰서 짧은 문자열 뒤에 빈칸(패딩)을 채우는 것이다.
DNA 분석에서는 모든 서열이 정확히 101bp 또는 21bp로 고정되어 있어서 이 문제가 없었다. 하지만 SMILES는 분자 구조에 따라 길이가 천차만별이므로 최대 길이를 알아내서 패딩 기준으로 쓰는 과정이 반드시 필요하다.
#
#4 정리
아직 모델을 만들지도 않았는데 벌써 중요한 준비 작업 세 가지를 마쳤다. 첫째, 수만 개의 분자를 SMILES 문자열로 확보했다. 둘째, 이 문자열들에 등장하는 모든 고유 문자를 모아서 모델의 어휘를 정의했다. 셋째, 가장 긴 문자열의 길이를 파악해서 입력 크기의 기준을 잡았다.
이 세 가지 정보, 즉 SMILES 목록과 토큰 어휘와 최대 길이가 있으면, 다음 단계에서 각 SMILES를 원-핫 인코딩으로 변환해서 VAE에 넣을 준비가 완료된다. DNA 서열을 (101, 4) 행렬로 변환했던 것처럼, SMILES도 (최대길이, 어휘크기) 행렬로 변환될 것이다. 표현 대상이 DNA에서 분자로 바뀌었을 뿐, “문자열을 숫자 행렬로 바꿔서 신경망에 넣는다"는 근본 전략은 동일하다.