[NLP][Code Review] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding pytorch 코드 리뷰
- 논문 링크 (98939회 인용)
Motivation
- pre-trained 언어 모델을 사용하는 것은 많은 nlp task에서 효과적이라고 밝혀졌다.
- downstream task에 대해 pre-trained 언어 모델을 사용하는 것은 크게 feature-based와 fine tuning 방법이 있다.
- feature-based 접근법 (ELMo)
- task 마다 다른 구조를 사용하고 pre-trained representations을 추가적인 피쳐로 사용한다.
- pre-trained 모델의 파라미터는 고정한다.
- ELMo와 같은 모델은 LSTM을 사용하긴 하지만 forward/backward output을 단순히 concat하는 것이기 때문에 shallowly bidirectional 하다고 할 수 있다.
- fine-tuning 접근법 (GPT)
- task 마다 서로 다른 파라미터 수를 최소화하고 pre-trained 모델의 파라미터와 함께 모두 학습을 진행한다.
- GPT와 같은 접근법은 unidirectional 하다. 즉, 여태까지 나온 단어를 기반으로 현재의 단어를 예측한다.
- feature-based 접근법 (ELMo)
- 특히 fine-tuning과 같이 left-to-right의 구조를 가지는 모델은 다양한 downstream task를 학습하기에는 좋지 않다.
- 이런 배경에서 bert는 unidirectional 학습 방향을 보안한 bidirectional transformer 구조를 제안한다.
Sumary
- BERT는 인풋의 일부 토큰을 마스킹하고 그 마스킹된 토큰을 예측하는 것을 objective function으로 설정한다.
- 단순히 양방향의 영향을 concat하는 unidirectional 학습법(shallowly bidirectional)과는 달리, 이러한 masked language modeling을 통해서 양방향의 영향을 종합적(deep bidirectional)으로 학습한다.
- 또한 문장간의 관계를 학습하기 위해 next sentence prediction도 사용한다.
Methodology
BERT는 transformer의 encoder 아키텍쳐를 그대로 차용한다. transformer의 encoder / decoder 아키텍쳐를 살펴보자.
encoder에서는 input embedding간의 관계를 학습(self attention)한 임베딩이 생성되고 decoder에서는 input과 output간의 관계를 학습한(cross attention) 임베딩이 생성된다. bert는 여기서 encoder만 사용한다는 것이다. 즉, bert는 학습을 진행할 때, output embedding이 따로 없다.
transformer 학습 과정을 간단하게 떠올려보자. transformer는 기계번역을 위해 연구된 방법이고 input은 번역하기 이전의 언어, output은 번역한 이후의 언어이다. transformer의 최종 레이어는 sequence의 각 위치에서 output vocab에 대한 softmax 확률을 구한다. 따라서 이에 대한 cross entropy loss을 구하고 gradient을 업데이트하는 방식으로 파라미터가 업데이트 된다. 그렇다면 bert는 어떤 식으로 목적 함수가 구성이 될까?
Masked language modeling
bert에서는 LSTM과 같이 left-to-right, right-to-left 학습법을 진정한 의미의 bidirectional이라고 보지는 않는다. 단순하게 concat한 것이기 때문이다. 대신, 토큰의 일부를 마스킹하고 이를 학습하는 방식을 사용하는데 이를 masked LM (MLM)이라고 한다. 논문에서는 전체 토큰의 15%만 마스킹한다고 언급한다.
예를 들어서 위와 같은 문장이 있다고 하자. bert는 문장 내에서 일부 단어를 랜덤하게 [MASK]로 대체하고 [MASK]된 단어를 맞추는 방식으로 목적 함수를 정하여 학습을 진행한다. 위 예시에서는 [MASK] 자리에 NLP, interesting이라는 단어가 맞게 오도록 학습하는 것이다.
다만, 이렇게 학습을 진행하게 되면 단점이 하나 있다. [MASK]를 예측하는 과정은 pre-training 단계인데 [MASK] 토큰 자체가 fine-tuning 단계에서 나타나지 않기 때문에 vocab 관점에서 pre-training, fine-tuning 단계의 mismatch가 발생할 수 있다. [MASK] 토큰은 pre-training 단계에서 임의로 만든 것이기 때문에 fine-tuning 단계에서는 [MASK] 토큰이 아예 나타날 수 없기 때문이다. 이를 보안하기 위해 항상 15% 전체를 마스킹하는 것이 아니라 아래와 같이 랜덤한 로직을 섞는다.
- 전체 토큰에서 15%을 선택했으면, 그 중 80% 토큰은 [MASK]로 대체한다.
- 전체 토큰에서 15%을 선택했으면, 그 중 10%는 랜덤한 토큰으로 대체한다.
- 전체 토큰에서 15%을 선택했으면, 그 중 10%(나머지)는 는 그대로 둔다.
bert는 mlm을 할 때, 전체 토큰에 대한 예측을 하는게 아니라 masking된 토큰만 예측을 진행한다. 따라서 15%의 확률로 토큰 $T_i$가 선택되었다면 [MASK] 또는 랜덤토큰 또는 변하지 않은 기존 토큰과 원래의 토큰과의 cross entropy을 계산하게 된다. 이러한 mlm을 통해서 나오는 cross entropy loss가 전체 loss의 일부로 들어간다.
그러면 코드 상으로 어떻게 구현이 되는지 살펴보자. (git code)
class BERTDataset:
def random_word(self, sentence):
tokens = sentence.split()
output_label = []
output = []
# 15% of the tokens would be replaced
for i, token in enumerate(tokens):
prob = random.random()
# remove cls and sep token
token_id = self.tokenizer(token)["input_ids"][1:-1]
if prob < 0.15:
prob /= 0.15
# 80% chance change token to mask token
if prob < 0.8:
for i in range(len(token_id)):
output.append(self.tokenizer.vocab["[MASK]"])
# 10% chance change token to random token
elif prob < 0.9:
for i in range(len(token_id)):
output.append(random.randrange(len(self.tokenizer.vocab)))
# 10% chance change token to current token
else:
output.append(token_id)
output_label.append(token_id)
else:
output.append(token_id)
for i in range(len(token_id)):
output_label.append(0)
# flattening
output = list(itertools.chain(*[[x] if not isinstance(x, list) else x for x in output]))
output_label = list(itertools.chain(*[[x] if not isinstance(x, list) else x for x in output_label]))
assert len(output) == len(output_label)
return output, output_label
BERTDataset 클래스는 bert pre-training에 필요한 데이터를 만든다. 원래 다른 전처리 메써드들이 많은데 편의를 위해 생략했다.
- sentence는 인풋으로 들어가는 텍스트, token은 띄어쓰기 기준으로 나눈 단어, token_id는 그에 해당하는 id이다. 토크나이징에 따라서 하나의 token에 대해 여러개의 token_id을 가질 수 있다.
- 반환해야할 값은 `output_label`과 `output`이다. `output_label`은 마스킹 단계에 들어갔다면 0이 아닌 해당 토큰의 id를 넣고 마스킹 단계에 들어가지 않았다면 0을 넣는다. (line 32, line 36~37)
- `output_label`은 mlm에서 bert가 cross entropy을 계산해야하는 위치를 저장한다.
- 난수를 뽑아서 15%의 확률로 마스킹 관련된 스텝을 밟을지 말지 결정한다. (line 15)
- 15%의 확률에 걸리는 경우 중에 80%, 10%, 10%의 확률로 다시 분기를 친다. (line 16)
- 80%의 확률로 마스킹을 한다. (line 19~21)
- 10%의 확률로 랜덤한 단어를 뽑는다. (line 24~26)
- 10%의 확률로 원래 단어를 넣는다. (line 30)
- token_id는 리스트이기 때문에 `output`에 그 길이만큼의 마스킹, 또는 랜덤 단어, 또는 원래의 단어를 넣는다.
Next sentence prediction
많은 NLP task에서 문장간의 관계가 중요한데 이는 language modeling으로 모델에 잘 녹여내기가 쉽지 않다. bert는 문장간의 관계나 의미도 녹여내기 위해서 binarized sentence prediction task을 진행한다. 단순하게 원래 (X,Y,Z) 순서로 네개의 문장이 있을 때, (X,Y)로 구성된 데이터는 1, (X,Z)로 구성된 데이터는 0의 값을 가지도록 데이터를 구성한다. 논문에서는 두 문장을 A,B라고 정의한다. A의 쌍에 맞는 문장을 고를 때, 50%의 확률로 B는 그 다음 문장을, 나머지 확률로 랜덤한 문장을 뽑는 것이다. 코드로 살펴보자.
class BERTDataset:
def get_sent(self, index):
t1, t2 = self.get_corpus_line(index)
if random.random() > 0.5:
return t1, t2, 1
else:
return t1, self.get_random_line(), 0
`self.get_corpus_line`는 연속하는 두 문장의 인덱스를 반환하는 메써드이다. 50%의 확률로 그 문장을 그대로 반환하고 나머지 확률로 랜덤한 문장의 인덱스와 `0`의 NSP label을 반환한다.
Preprocess for bert input
앞서, bert는 두 문장을 합쳐서 데이터를 구성한다고 했다. bert는 이때, [CLS], [SEP] 토큰과 같은 special 토큰을 같이 넣는다. 구체적인 예시로 살펴보자.
my dog is cute이라는 문장과 he likes playing in my garden이라는 문장으로 하나의 input 데이터를 구성했다. 두 문장을 연결할 때, [SEP] 토큰을, 그리고 두 문장을 연결하고 마지막에 [SEP] 토큰을 넣는다. 또한 문장의 시작 부분에 [CLS] 토큰을 넣는다. 이 [CLS] 토큰은 NSP에서 두 문장이 연속됐는지, 아닌지 여부에 대한 cross entropy loss 계산시에 사용된다.
- token embeddings: 말 그대로 token에 대한 임베딩이다.
- segment embeddings: 해당하는 토큰이 두 문장 중 어느 문장에 속하는지 알려준다.
- position embeddings: 토큰의 위치에 대한 정보를 담고 있는 임베딩이다.
- output label: mlm에서 사용되는 label로, 마스킹된 부분은 해당하는 토큰의 token_id를 담고 마스킹되지 않은 부분은 전부 0이다. mlm에서 마스킹된 부분에 대해서만 예측을 진행하기 때문에 output label을 이런 식으로 구성한다.
- is next sent: 두 문장이 연속되는 문장인지에 대한 여부를 의미한다.
bert에 input을 전처리하는 과정은 transformer에 비해서 더 까다롭다. 코드로 살펴보자.
class BERTDataset:
def __getitem__(self, item):
# step 1: get random sentence pair, either negative or positive (saved as is_next_label)
t1, t2, is_next_label = self.get_sent(item)
# step 2: replace random words in sentence with mask / random words
t1_random, t1_label = self.random_word(t1)
t2_random, t2_label = self.random_word(t2)
# step 3
t1 = [self.tokenizer.cls_token_id] + t1_random + [self.tokenizer.sep_token_id]
t2 = t2_random + [self.tokenizer.sep_token_id]
t1_label = [self.tokenizer.pad_token_id] + t1_label + [self.tokenizer.pad_token_id]
t2_label = t2_label + [self.tokenizer.pad_token_id]
# step 4
segment_label = ([1 for _ in range(len(t1))] + [2 for _ in range(len(t2))])[:self.seq_len]
bert_input = (t1 + t2)[:self.seq_len]
bert_label = (t1_label + t2_label)[:self.seq_len]
padding = [self.tokenizer.pad_token_id for _ in range(self.seq_len - len(bert_input))]
bert_input += padding
bert_label += padding
segment_label += padding
output = {
"bert_input": bert_input,
"bert_label": bert_label,
"segment_label": segment_label,
"is_next": is_next_label
}
return {key: torch.tensor(value) for key, value in output.items()}
- line 5: 두 문장을 뽑고 `is_next_label`도 같이 받는다. 이 값은 NSP의 binary prediction에 사용되는 값이다.
- line 8~9: 랜덤하게 마스킹된 단어를 뽑는다. 이때, `output_label`을 통해 어디가 마스킹됐는지 알 수 있다.
- line 12~15: A 문장 앞뒤로 [CLS], [SEP] 토큰을, B 문장 뒤에 [SEP] 토큰을 넣는다. output_label은 [CLS], [SEP]가 아니라 [PAD] 토큰을 넣는다.
- line 18: segment label을 구성한다. A 문장은 1, B 문장은 2로 표시한다.
- line 19~24: `bert_input`과 `bert_label`을 구성한다. 이때, `self.seq_len`을 통해서 max_len만큼 자른다.
최종 반환되는 딕셔너리를 다시 한번 살펴보자.
- `bert_input`: 마스킹된 토큰으로, [CLS], [SEP], [MASK]과 token_id을 포함한다.
- `bert_label`: 마스킹 여부를 알려주는 값으로 마스킹 되었다면 해당하는 token_id로, 그렇지 않다면 0으로 표시한다.
- `segment_label`: 두 문장을 알려주는 값이다.
- `is_next`: 두 문장이 연속하는지 알려주는 값이다.
Bert architecture
서두에 bert는 transformer 구조에서 encoder만 가져온 모델이라고 언급했지만 activation function, objective function에서 차이점을 보인다. transformer encoder의 구체적인 설명은 아래 포스팅에서 해뒀으니 넘어가도록 하고, 본 포스팅에서는 차이점에 대해서만 짚고 넘어가고자 한다.
https://steady-programming.tistory.com/82
[NLP][Code Review] Attention is all you need pytorch 코드 리뷰
논문링크(NIPS, 113405회 인용) Paper motivation RNN은 sequential한 데이터를 처리하는데 있어서 시간의 순서에 따른 hidden state인 $h_{t}$을 $h_{t-1}, t$의 함수로 만든다. 즉, 현재의 $h_t$을 만들기 위해 이전 hid
steady-programming.tistory.com
Input embedding
transformer에서는 input embedding이 token embedding과 positional embedding이 합쳐져서 들어간다. 코드로 살펴보자.
import torch
import torch.nn as nn
import numpy as np
class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# not a parameter
self.register_buffer("pos_table", self._get_sinusoid_encoding_table(n_position, d_hid))
def _get_sinusoid_encoding_table(self, position, d_hid):
def get_position_angle_vec(position, d_hid):
return [position / np.power(10000, 2*(i // 2) / d_hid) for i in range(d_hid)]
sinusoid_table = np.array(
[get_position_angle_vec(pos, d_hid) for pos in range(position)]
)
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:,0::2])
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:,1::2])
return torch.FloatTensor(sinusoid_table).unsqueeze(0)
def forward(self, x):
# self.pos_table.shape = (1, max_length, d_emb)
# x.shape = (batch_size, seq_length, d_emb)
return x + self.pos_table[:, :x.size(1)].clone().detach()
클래스가 만들어짐과 동시에 `pos_table`에 position_table을 저장한다. `forward` 메써드에는 입력으로 들어오는 input에 맞는 positional embedding을 더하여 반환한다.
위에서 살펴봤을 때, bert는 `segment_label`이라는 것이 하나 더 추가된다. `segment_label`이란, bert의 입력으로 들어가는 두 문장을 구분해주는 라벨로, 이를 따로 임베딩화하여 넣어줘야한다는 것이다. 코드로 살펴보자.
import torch.nn as nn
from attention.modules.position_encoding import PositionalEncoding
class BERTEmbedding(nn.Module):
def __init__(self,
embed_size,
seq_len=64):
super().__init__()
self.embed_size = embed_size
# (batch_size, seq_len) -> (batch_size, seq_len, embed_size)
self.segment = nn.Embedding(3, embed_size, padding_idx=0)
self.position = PositionalEncoding(d_hid=embed_size, n_position=seq_len)
def forward(self, x, segment_label):
x = self.position(x) + self.segment(segment_label)
return x
`self.segment`가 segment_label을 임베딩화하는 클래스이다. 잘보면 `nn.Embedding`의 `vocab_size`가 3으로 지정되어 있는 것을 알 수 있다. 언뜻보면 bert input은 두개의 문장을 넣으므로 두개의 라벨만 붙이면 되는 것 아닌가? 라는 생각이 들었는데, 전처리할 때 문장에 대한 label을 1과 2로 붙여서 `vocab_size`도 3으로 한게 아닌가 싶다. 3으로 해야 index 0부터 시작해서 index 2까지 임베딩 행렬이 만들어지기 때문이다.
bert의 input으로 sequence인 `x`와 `segment_label`이 들어가면 `self.position(x) + self.segment(segment_label)`을 통해 token embedding + positional embedding + segment embedding 모두가 더해져서 나온다. 이것이 바로 bert encoder의 input으로 들어가는 것이다.
Activation function
transformer의 구조에서 activation function이 어디에 쓰이는지 생각해보자. sublayer로는 multi-head attention과 feed-forward network가 있다. 여기서 ffw layer에서 linear function 이후에 activation function이 사용된다. transformer는 relu function을 사용하는 반면에 bert에서는 gelu function을 사용한다고 논문에 언급되어 있다.
We use a gelu activation (Hendrycks and Gimpel, 2016) rather than the standard relu, following OpenAI GPT.
gpt에서 gelu를 썼기 때문에 이를 따른다고 한다. 코드로 살펴보자.
import torch.nn as nn
import torch.nn.functional as F
class PositionwiseFeedForward(nn.Module):
def __init__(self,
d_in,
d_hid,
activation,
dropout=0.1):
super().__init__()
self.activation = activation
self.fc1 = nn.Linear(d_in, d_hid)
self.fc2 = nn.Linear(d_hid, d_in)
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(p=dropout)
def forward(self, x):
residual = x
x = self.fc2(self.activation(self.fc1(x)))
x = self.dropout(x)
x = self.layer_norm(x + residual)
return x
transformer이든, bert이든, ffw는 FC > activation > FC의 과정을 포함한다. 또한 `activation` 파라미터로 어떤 함수를 사용할지 받도록 했다. 만약 transformer인 경우에는 `torch.nn.functional.relu`를, bert인 경우에는 `torch.nn.functional.gelu`를 사용하는 것이다.
Objective function
objective function을 보기 전에, bert의 encoder 구조를 보고 가자.
class Encoder(nn.Module):
def __init__(self,
n_vocab,
d_emb,
d_model,
d_k,
d_v,
n_head,
d_hid,
n_position,
n_layers):
super().__init__()
self.embedding = nn.Embedding(n_vocab, d_emb, padding_idx=0)
self.bert_embedding = BERTEmbedding(embed_size=d_emb,
seq_len=n_position)
self.layer_stack = nn.ModuleList(
[
EncoderLayer(n_head,
d_model,
d_emb,
d_k,
d_v,
d_hid,
activation=F.gelu,
dropout=0.1) for _ in range(n_layers)
]
)
def forward(self, seq, segment_label, mask, return_attns=False):
enc_slf_attn_list = []
enc_output = self.bert_embedding(self.embedding(seq), segment_label)
for layer in self.layer_stack:
enc_output, enc_slf_attn = layer(enc_output, mask)
if return_attns:
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output,
`EncoderLayer`는 multi-head attention, feed-forward network로 이루어져 있고 transformer와 마찬가지로 $n_layers$ 만큼 모델에 layer을 쌓는다. encoder에 최초로 들어가는 입력값은 token_id로 이루어진 `seq`, 두 문장을 구분지어주는 `segment_label` 이다. `seq`와 `segment_label`의 차원은 모두 (batch_size, seq_len)이다. `n_layers`개만큼 encoding이 된 후에는 `enc_output`을 얻는데, 차원은 (batch_size, seq_len, d_model)이다. multi-head attention을 했기 때문에, 중간에 (batch_size, n_head, seq_len, d_v) 차원을 거쳐서 (batch_size, seq_len, d_model)이 된 것이다. transformer와 거의 동일한 encoding 단계이지만, bert는 입력값을 `segmet_label`이 들어가는 것이 다른 점이다.
bert는 transformer와는 다르게 decoder가 없다고 했다. encoder로만 구성된 bert의 전체적인 구조를 코드로 살펴보자.
class Bert(nn.Module):
def __init__(self,
n_vocab,
d_emb,
d_model,
d_k,
d_v,
n_head,
d_hid,
n_position,
n_layers):
super().__init__()
self.encoder = Encoder(
n_vocab=n_vocab,
d_emb=d_emb,
d_model=d_model,
d_k=d_k,
d_v=d_v,
n_head=n_head,
d_hid=d_hid,
n_position=n_position,
n_layers=n_layers
)
def forward(self, seq, segment_label):
mask = (seq > 0).unsqueeze(1)
enc_output, *_ = self.encoder(seq, segment_label, mask)
return enc_output
`mask`를 살펴보자. transformer에서는 encoder의 마스킹은 padding에 해당하는 토큰은 0으로 만들어주었고 decoder의 마스킹은 subsequent 마스킹이었다. bert에서 encoder의 마스킹은 transformer의 마스킹과 동일하게 적용한다. 즉, padding에 해당하는 토큰만 마스킹을 해주는 것이다. `bert` 클래스의 최종 출력값은 `enc_output`으로 차원은 (batch_size, seq_len, d_model)이다. 여기서 transformer는 `nn.Linear(d_model, n_trg_vocab)`을 통해 logit을 만들고 softmax 함수를 통해서 cross entropy loss을 구한다.
bert는 1) masked language model 2) next sentence prediction 두가지 task을 수행한다. bert의 mlm은 transformer의 cross entropy loss와 거의 맥락상 유사하다. transformer는 decoder의 target_vocab에 대해서, bert는 학습하는 vocab에 대해서 softmax을 구하는 것이 차이점이다. mlm layer을 살펴보자.
class MaskedLanguageModel(nn.Module):
def __init__(self, hidden, vocab_size):
super().__init__()
self.linear = nn.Linear(hidden, vocab_size)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, x):
return self.softmax(self.linear(x))
- 우선 hidden > vocab_size만큼 linear layer을 적용한다.
- 여기서 `x`는 encoder의 최종 출력물으로, 차원은 (batch_size, seq_len, d_model)이다.
- `self.softmax(self.linear(x))`의 차원은 (batch_size, seq_len, vocab_size)이다.
- 여기서 가장 큰 softmax 값을 가지는 token이 마스킹된 자리의 토큰으로 예측되는 것이다.
다음으로 nsp layer을 살펴보자.
class NextSentencePrediction(nn.Module):
def __init__(self, hidden):
super().__init__()
self.linear = nn.Linear(hidden, 2)
self.softmax = nn.LogSoftmax(dim=-1)
def forward(self, x):
# use only the first token which is the [CLS]
# x.shape = (batch_size, seq_len, d_model)
# to use only the first token, use x[:,0,:]
return self.softmax(self.linear(x[:,0,:]))
- `x`의 차원은 (batch_size, seq_len, d_model)이다.
- 여기서 중요한 점은 `self.linear`을 태울 때, 입력값으로 `x`가 아니라 `x[:,0,:]`가 들어간다는 것이다.
- nsp을 할 때, 다른 토큰은 사용되지 않고 오직 `[CLS]` 토큰만 사용된다. 데이터를 구성할 때, `[CLS]` 토큰은 문장의 가장 첫부분에 위치시켰으므로, 첫번째 인덱스만 골라내는 것이다.
- 이후는 softmax을 적용하는 부분으로, 2차원의 확률값이 나오고 다음 문장이 맞는지, 아닌지 여부의 의미를 가진다.
bert는 nsp, mlm layer을 거쳐서 최종적으로 두 출력물을 반환한다. 코드로 살펴보자.
class Model(nn.Module):
def __init__(self,
n_vocab,
d_word_vec=512,
d_model=512,
d_k=64,
d_v=64,
n_head=8,
d_inner=2048,
n_position=200,
n_layers=6,
**kwargs):
super().__init__()
self.bert = Bert(
n_vocab,
d_word_vec,
d_model,
d_k,
d_v,
n_head,
d_inner,
n_position,
n_layers
)
self.next_sentence = NextSentencePrediction(d_model)
self.mask_lm = MaskedLanguageModel(d_model, n_vocab)
def forward(self, x, segment_label):
x = self.bert(x, segment_label)
return self.next_sentence(x), self.mask_lm(x)
bert을 training할 때, nsp, mlm 모두에서 loss을 계산해야한다. 잘 생각해보면 두 layer는 모두 softmax을 사용하므로 각각에서 cross entropy loss을 구한 뒤, 이를 더하면 된다. 그리고 그에 따라서 gradient descent을 수행한다.
Conclusion
transformer에 이어서 bert도 구현된 pytorch 코드를 보며 디버깅을 하고, 모델 구조를 살펴보았다. 시간은 오래 걸리긴 하지만, 이렇게 공부를 하니까 머리속에 훨씬 더 잘 남고 코드 흐름도 잘 파악되는 것 같다. 특히 bert는 transformer의 encoder 부분만 뜯어온 것이지만 그 가운데 다른점도 존재하므로 그 점에 집중해서 공부한 것이 도움이 되었다. 다음에는 transformer의 decoder 부분만 뜯어온 gpt를 살펴볼 것이다. 그 다음에는 llama, mistral, phi 등.. llm을 하나하나 살펴보도록 하자. 본 포스팅에 사용된 코드와 전체 training 코드는 여기에서 확인할 수 있다. PR은 언제나 환영이다.