STEP 15 / 18

진짜 데이터: 토큰화와 사전

3부에서는 Multi30k 독일어→영어 번역을 학습한다. 이미지 설명 문장 약 3만 쌍이라 논문의 WMT(450만 쌍)보다 훨씬 작지만, 전체 흐름을 보기에는 충분하다.

  • load_tokenizers: spaCy의 독일어·영어 모델로 문장을 단어 단위로 자른다.
  • build_vocabulary: train+valid+test 전체에서 두 번 이상 나온 단어로 사전을 만든다. 맨 앞 네 칸은 특수 토큰이다: <s>(0, 시작), </s>(1, 끝), <blank>(2, 패딩), <unk>(3, 모르는 단어). 그래서 앞 단계 코드들이 pad=2, start_symbol=0을 기본값으로 쓴다.
  • load_vocab: 만든 사전을 vocab.pt에 저장해 두고 다음부터는 불러온다.

원본은 torchtext의 Multi30k, build_vocab_from_iterator, to_map_style_dataset을 쓴다. torchtext는 개발이 중단돼 최신 torch에서 import조차 되지 않는다. 그래서 같은 이름·같은 동작의 대체품을 파일 안에 넣었다(diff의 # torchtext 대신 부분). 데이터는 같은 분할을 Hugging Face Hub의 bentrevett/multi30k에서 받는다. torch 2.6부터 torch.load의 기본값이 바뀌어서 사전을 불러올 때 weights_only=False가 필요하다.

실행 결과에서 볼 것

  • 29,000 / 1,014 / 1,000 문장쌍.
  • 사전 크기: 독일어 8,316, 영어 6,384. 원본이 torchtext로 만든 것과 같은 순서다.
  • 사전은 특수 토큰 4개 다음에 가장 흔한 토큰부터 온다. 독일어는 ., Ein, einem, 영어는 a, ., A 순서다. 대소문자를 구분하므로 a와 A가 다른 단어다.

코드 변화

+129 −0 · 파일 전체 632줄

이전 단계의 transformer.py와 비교했다. 초록 줄이 새로 생긴 줄, 빨간 줄이 사라진 줄이다.

GitHub에서 보기 ↗
1+import os
2+from os.path import exists
13import copy
24import math
35import time
6+from collections import Counter
47
8+import spacy
59import torch
610import torch.nn as nn
711from torch.nn.functional import log_softmax
501505 max_len = src.shape[1]
502506 src_mask = torch.ones(1, 1, max_len)
503507 print(greedy_decode(model, src, src_mask, max_len=max_len, start_symbol=0))
508+
509+
510+# torchtext 대신 (torchtext는 개발이 중단돼 최신 torch에서 import되지 않는다)
511+def Multi30k(language_pair=("de", "en")):
512+ "torchtext.datasets.Multi30k와 같은 29,000 / 1,014 / 1,000 문장쌍"
513+ from datasets import disable_progress_bars, load_dataset
514+
515+ disable_progress_bars()
516+ ds = load_dataset("bentrevett/multi30k", cache_dir=".data")
517+ src, tgt = language_pair
518+ return tuple(
519+ list(zip(ds[split][src], ds[split][tgt]))
520+ for split in ("train", "validation", "test")
521+ )
522+
523+
524+class Vocab:
525+ "torchtext.vocab.Vocab 중 원본이 쓰는 부분"
526+
527+ def __init__(self, itos):
528+ self.itos = list(itos)
529+ self.stoi = {t: i for i, t in enumerate(self.itos)}
530+ self.default_index = None
531+
532+ def __len__(self):
533+ return len(self.itos)
534+
535+ def __getitem__(self, token):
536+ return self.stoi.get(token, self.default_index)
537+
538+ def __call__(self, tokens):
539+ return [self[t] for t in tokens]
540+
541+ def set_default_index(self, index):
542+ self.default_index = index
543+
544+ def get_stoi(self):
545+ return self.stoi
546+
547+ def get_itos(self):
548+ return self.itos
549+
550+
551+def build_vocab_from_iterator(iterator, min_freq=1, specials=None):
552+ "specials 먼저, 나머지는 빈도 내림차순(같으면 사전순). torchtext 0.12와 같은 순서"
553+ counter = Counter()
554+ for tokens in iterator:
555+ counter.update(tokens)
556+ specials = specials or []
557+ for s in specials:
558+ counter.pop(s, None)
559+ ordered = sorted(counter.items(), key=lambda x: (-x[1], x[0]))
560+ return Vocab(specials + [w for w, c in ordered if c >= min_freq])
561+
562+
563+def to_map_style_dataset(iter_data):
564+ "DistributedSampler가 len()을 요구해서 원본에 있던 함수"
565+ return list(iter_data)
566+
567+
568+def load_tokenizers():
569+
570+ try:
571+ spacy_de = spacy.load("de_core_news_sm")
572+ except IOError:
573+ os.system("python -m spacy download de_core_news_sm")
574+ spacy_de = spacy.load("de_core_news_sm")
575+
576+ try:
577+ spacy_en = spacy.load("en_core_web_sm")
578+ except IOError:
579+ os.system("python -m spacy download en_core_web_sm")
580+ spacy_en = spacy.load("en_core_web_sm")
581+
582+ return spacy_de, spacy_en
583+
584+
585+def tokenize(text, tokenizer):
586+ return [tok.text for tok in tokenizer.tokenizer(text)]
587+
588+
589+def yield_tokens(data_iter, tokenizer, index):
590+ for from_to_tuple in data_iter:
591+ yield tokenizer(from_to_tuple[index])
592+
593+
594+def build_vocabulary(spacy_de, spacy_en):
595+ def tokenize_de(text):
596+ return tokenize(text, spacy_de)
597+
598+ def tokenize_en(text):
599+ return tokenize(text, spacy_en)
600+
601+ print("Building German Vocabulary ...")
602+ train, val, test = Multi30k(language_pair=("de", "en"))
603+ vocab_src = build_vocab_from_iterator(
604+ yield_tokens(train + val + test, tokenize_de, index=0),
605+ min_freq=2,
606+ specials=["<s>", "</s>", "<blank>", "<unk>"],
607+ )
608+
609+ print("Building English Vocabulary ...")
610+ train, val, test = Multi30k(language_pair=("de", "en"))
611+ vocab_tgt = build_vocab_from_iterator(
612+ yield_tokens(train + val + test, tokenize_en, index=1),
613+ min_freq=2,
614+ specials=["<s>", "</s>", "<blank>", "<unk>"],
615+ )
616+
617+ vocab_src.set_default_index(vocab_src["<unk>"])
618+ vocab_tgt.set_default_index(vocab_tgt["<unk>"])
619+
620+ return vocab_src, vocab_tgt
621+
622+
623+def load_vocab(spacy_de, spacy_en):
624+ if not exists("vocab.pt"):
625+ vocab_src, vocab_tgt = build_vocabulary(spacy_de, spacy_en)
626+ torch.save((vocab_src, vocab_tgt), "vocab.pt")
627+ else:
628+ vocab_src, vocab_tgt = torch.load("vocab.pt", weights_only=False)
629+ print("Finished.\nVocabulary sizes:")
630+ print(len(vocab_src))
631+ print(len(vocab_tgt))
632+ return vocab_src, vocab_tgt

실행 결과

실행한 코드 demo.py 보기
1from transformer import Multi30k, load_tokenizers, load_vocab, tokenize
2
3spacy_de, spacy_en = load_tokenizers()
4
5train, val, test = Multi30k(language_pair=("de", "en"))
6print(f"Multi30k: train {len(train):,} / valid {len(val):,} / test {len(test):,} 문장쌍")
7print()
8de, en = train[0]
9print("첫 문장쌍")
10print(" de:", de)
11print(" en:", en)
12print(" de 토큰:", tokenize(de, spacy_de))
13print(" en 토큰:", tokenize(en, spacy_en))
14print()
15
16vocab_src, vocab_tgt = load_vocab(spacy_de, spacy_en)
17print()
18print("독일어 사전 앞 20개:", vocab_src.get_itos()[:20])
19print("영어 사전 앞 20개 :", vocab_tgt.get_itos()[:20])
20print()
21tokens = tokenize(en, spacy_en)
22print("영어 문장 → id:", vocab_tgt(tokens))
23print("사전에 없는 단어 'Transformerology' →", vocab_tgt["Transformerology"], "(<unk>)")
Multi30k: train 29,000 / valid 1,014 / test 1,000 문장쌍

첫 문장쌍
  de: Zwei junge weiße Männer sind im Freien in der Nähe vieler Büsche.
  en: Two young, White males are outside near many bushes.
  de 토큰: ['Zwei', 'junge', 'weiße', 'Männer', 'sind', 'im', 'Freien', 'in', 'der', 'Nähe', 'vieler', 'Büsche', '.']
  en 토큰: ['Two', 'young', ',', 'White', 'males', 'are', 'outside', 'near', 'many', 'bushes', '.']

Building German Vocabulary ...
Building English Vocabulary ...
Finished.
Vocabulary sizes:
8316
6384

독일어 사전 앞 20개: ['<s>', '</s>', '<blank>', '<unk>', '.', 'Ein', 'einem', 'in', 'und', ',', 'mit', 'auf', 'Mann', 'einer', 'Eine', 'ein', 'der', 'Frau', 'eine', 'die']
영어 사전 앞 20개 : ['<s>', '</s>', '<blank>', '<unk>', 'a', '.', 'A', 'in', 'the', 'on', 'is', 'and', 'man', 'of', 'with', ',', 'woman', 'are', 'to', 'Two']

영어 문장 → id: [19, 25, 15, 1138, 822, 17, 58, 84, 335, 1359, 5]
사전에 없는 단어 'Transformerology' → 3 (<unk>)