ALBERT: A Lite BERT for Self-supervised Learning of Language Representations 논문리뷰
Google Machine Learning Bootcamp 2022 에서 "NLP 논문 리뷰 스터디" 에 참여하며 정리한 자료입니다
ALBERT 의 기법으로 대용량 모델을 학습했고, BERT-LARGE 보다 적은 수의 파라미터를 가지지만 더 나은 성능을 보여주었다.
ALBERT는 메모리제한 문제 해결 + 학습 속도 향상
모델 내에서 Embedding vector 을 context 측면에서 두 가지로 분류할 수 있다.
그러나, $E = H$ 이므로, 모델의 Hidden size를 증가시키고자 할 때, embedding layer의 파라미터가 지나치게 크게 발생하는 문제 존재한다. 즉, Hidden size의 조정이 Embedding vector의 dimension과 분리되지 못하고 있다
이를 위해, Embedding matrix를 두 개의 작은 matrix로 분해한다
hidden size를 증가하는 것과 관계 없이, embedding dimension 을 설정할 수 있어, embedding layer의 파라미터 수의 지나친 증가가 필수적이지 않게된다 (Parameter Reduction)
이를 수식으로 나타내면,
ex.
추가데이터 사용 (XLNet, RoBERTa 데이터)
Dropout 제거
즉, 추가데이터 사용하고, Dropout 제거하는 것이 Pre-train에 도움된다
연속된 segment-pair (token_a, token_b) 에 대해 일정확률로 순서를 swap 해준다. 이 때, segment-pair는 최대길이를 넘지 않는 않게 구성한다.
# input 구성
is_next = rand() < 0.5 # whether token_b is next to token_a or not
# segment 단위 : len_tokens 만큼의 token (완전한 문장 단위X)
tokens_a = self.read_tokens(self.f_pos, len_tokens, True)
seek_random_offset(self.f_neg)
#f_next = self.f_pos if is_next else self.f_neg
f_next = self.f_pos # `f_next` should be next point
tokens_b = self.read_tokens(f_next, len_tokens, False)
if tokens_a is None or tokens_b is None: # end of file
self.f_pos.seek(0, 0) # reset file pointer
return
# sentence-order prediction (SOP)
instance = (is_next, tokens_a, tokens_b) if is_next \
else (is_next, tokens_b, tokens_a)
아래는 BERT 구현 코드이다. Embedding layer 이 Vocab_num X Dimension 으로 차원의 수가 hidden_num과 분리되지 않으며, 많은 수의 parameter로 구성될 수 밖에 없는 구조이다. (V x D = 30522 x 768 = 23440896)
# BERT Config
class Config(NamedTuple):
"Configuration for BERT"
vocab_size: int = 30522 # Size of Vocabulary
dim: int = 768 # Dimension of Hidden Layer in Transformer Encoder
n_layers: int = 12 # Numher of Hidden Layers
n_heads: int = 12 # Numher of Heads in Multi-Headed Attention Layers
dim_ff: int = 768*4 # Dimension of Intermediate Layers in Positionwise Feedforward Net
#activ_fn: str = "gelu" # Non-linear Activation Function Type in Hidden Layers
p_drop_hidden: float = 0.1 # Probability of Dropout of various Hidden Layers
p_drop_attn: float = 0.1 # Probability of Dropout of Attention Layers
max_len: int = 512 # Maximum Length for Positional Embeddings
n_segments: int = 2 # Number of Sentence Segments
# BERT : Embedding Matrix (V x D)
class Embeddings(nn.Module):
"The embedding module from word, position and token_type embeddings."
def __init__(self, cfg):
super().__init__()
# (V x D) : 30522 x 768 = 23440896
self.tok_embed = nn.Embedding(cfg.vocab_size, cfg.dim) # token embedding
self.pos_embed = nn.Embedding(cfg.max_len, cfg.dim) # position embedding
self.seg_embed = nn.Embedding(cfg.n_segments, cfg.dim) # segment(token type) embedding
self.norm = LayerNorm(cfg)
self.drop = nn.Dropout(cfg.p_drop_hidden)
def forward(self, x, seg):
seq_len = x.size(1)
pos = torch.arange(seq_len, dtype=torch.long, device=x.device)
pos = pos.unsqueeze(0).expand_as(x) # (S,) -> (B, S)
e = self.tok_embed(x) + self.pos_embed(pos) + self.seg_embed(seg)
return self.drop(self.norm(e))
아래는 ALBERT의 구현 코드이다. Embedding layer에서, 기존의 VxD의 큰 parameter matrix를 두 개의 작은 matrix로 분해했다. (V x E, E x D) 이를 통해, embedding layer에서의 dimension 수를 hidden_num과 다르게 지정할 수 있는 구조가 되어, embedding layer에서의 parameter reduction 효과를 얻을 수 있게 되었다. (23440896 -> 4005120, 약 5.8x 감소)
# ALBERT Config
class Config(NamedTuple):
"Configuration for ALBERT"
vocab_size: int = 30522 # Size of Vocabulary
hidden: int = 768 # Dimension of Hidden Layer in Transformer Encoder
hidden_ff: int = 768*4 # Dimension of Intermediate Layers in Positionwise Feedforward Net
embedding: int = 128 # Factorized embedding parameterization
n_layers: int = 12 # Numher of Hidden Layers
n_heads: int = 768//64 # Numher of Heads in Multi-Headed Attention Layers
#activ_fn: str = "gelu" # Non-linear Activation Function Type in Hidden Layers
max_len: int = 512 # Maximum Length for Positional Embeddings
n_segments: int = 2 # Number of Sentence Segments
# ALBERT : 2 Embedding Matrix (V x E + E x D)
class Embeddings(nn.Module):
"The embedding module from word, position and token_type embeddings."
def __init__(self, cfg):
super().__init__()
# Original BERT Embedding
# self.tok_embed = nn.Embedding(cfg.vocab_size, cfg.hidden) # token embedding
# factorized embedding
# (V x E + E x D) : (128 x 30522) + (128 x 768) = 4005120
self.tok_embed1 = nn.Embedding(cfg.vocab_size, cfg.embedding)
self.tok_embed2 = nn.Linear(cfg.embedding, cfg.hidden)
self.pos_embed = nn.Embedding(cfg.max_len, cfg.hidden) # position embedding
self.seg_embed = nn.Embedding(cfg.n_segments, cfg.hidden) # segment(token type) embedding
self.norm = LayerNorm(cfg)
# self.drop = nn.Dropout(cfg.p_drop_hidden)
def forward(self, x, seg):
seq_len = x.size(1)
pos = torch.arange(seq_len, dtype=torch.long, device=x.device)
pos = pos.unsqueeze(0).expand_as(x) # (S,) -> (B, S)
# factorized embedding
e = self.tok_embed1(x)
e = self.tok_embed2(e)
e = e + self.pos_embed(pos) + self.seg_embed(seg)
#return self.drop(self.norm(e))
return self.norm(e)
모든 layer에 대해 동일한 parameter 들에 propagation을 수행하였다. 아래의 BERT에서는 layer(block)을 이루는 파라미터들을 다르게 사용하였고, 이로 인해 전체 파라미터 수가 상대적으로 많았다. 반면 ALBERT의 코드를 보면, 깊은 layer에서도, 동일한 파라미터에 대해 연산이 이루어져, 여러 layer 사용에도 propagation 수가 늘어날 뿐, 결국 한 개 layer(block) 의 파라미터 수만큼만 사용됨을 알 수 있다.
# BERT : individual block (different parameter)
class Transformer(nn.Module):
""" Transformer with Self-Attentive Blocks"""
def __init__(self, cfg):
super().__init__()
self.embed = Embeddings(cfg)
self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)])
def forward(self, x, seg, mask):
h = self.embed(x, seg)
for block in self.blocks:
h = block(h, mask)
return h
class Block(nn.Module):
""" Transformer Block """
def __init__(self, cfg):
super().__init__()
self.attn = MultiHeadedSelfAttention(cfg)
self.proj = nn.Linear(cfg.dim, cfg.dim)
self.norm1 = LayerNorm(cfg)
self.pwff = PositionWiseFeedForward(cfg)
self.norm2 = LayerNorm(cfg)
self.drop = nn.Dropout(cfg.p_drop_hidden)
def forward(self, x, mask):
h = self.attn(x, mask)
h = self.norm1(x + self.drop(self.proj(h)))
h = self.norm2(h + self.drop(self.pwff(h)))
return h
# ALBERT : Parameter sharing
class Transformer(nn.Module):
""" Transformer with Self-Attentive Blocks"""
def __init__(self, cfg):
super().__init__()
self.embed = Embeddings(cfg)
# Original BERT not used parameter-sharing strategies
# self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)])
# To used parameter-sharing strategies
self.n_layers = cfg.n_layers
self.attn = MultiHeadedSelfAttention(cfg)
self.proj = nn.Linear(cfg.hidden, cfg.hidden)
self.norm1 = LayerNorm(cfg)
self.pwff = PositionWiseFeedForward(cfg)
self.norm2 = LayerNorm(cfg)
# self.drop = nn.Dropout(cfg.p_drop_hidden)
def forward(self, x, seg, mask):
h = self.embed(x, seg)
for _ in range(self.n_layers):
# h = block(h, mask)
h = self.attn(h, mask)
h = self.norm1(h + self.proj(h))
h = self.norm2(h + self.pwff(h))
return h
Q. Intrinsic task (SOP, NSP) 의 측정은 어떻게 이루어졌을까?
A. SQuAD, RACE 의 dev-set에 대해 각 pre-train objective (SOP, NSP) 에 맞게 Inference 를 진행할 수 있는 형태로 재구성했을 것으로 보인다. input으로 활용하기 위해 각 objective에 맞게 positive, negative sample들을 생성하고, 원본 text를 target으로 하여, 성능을 측정할 수 있었을 것이다.
Q. Paper의 초기버전과 최종버전을 비교했을 때, Result 부분에서, BERT-xlarge 를 추가한 것을 볼 수 있다. 이것은 어떤 의미일까.
A. 초기버전에서는 ALBERT-xxlarge 와의 성능은 BERT-large와 비교하는데, 학습 속도는 BERT-base와 비교하여, 비교에서의 일관성이 낮다고 느껴졌었다. 그런데 성능 및 학습 시간 모두 BERT-xlarge 와 비교하여 더 좋은 결과를 가진다는 것을 보여주게 되어, 비교의 객관성을 보완하고, 모델의 우수성을 더 효과적으로 어필했다고 느껴졌다
BERT의 저자들 중 NSP 사용에 기여한 사람은 누굴까.. 그는 대체 어떤 죄를 지었기에 이토록 많은 논문을 통해 고통받아야 하는가. (혹시 다수의 Citation을 위한 큰 그림은 아니겠지.....? 만약 그렇다면 역시 "Google Brain"....
<Reference>
Lan, Z., Chen, M., Goodman, S., Gimpel, K., Sharma, P., & Soricut, R. (2019). Albert: A lite bert for self-supervised learning of language representations. arXiv preprint arXiv:1909.11942
댓글 영역