Post

[Paper]BERT

2018년에 구굴의 연구팀에 의해 발표된 BERT(Bidirectional Encoder Representations from Transforemr)는 Transfer learning을 통해 NLP에서 SOTA의 결과를 이끌어낸 아키텍처 중 하나입니다.

양방향으로 context를 받아 언어 모델링에 자연스럽게 맞지 않는 이유입니다. 이러한 목적에 관해서 학습하기 위해 masked 언어 모델링이 제안되었습니다. 주된 아이디어는 입력 단어의 부분을 가려 모델이 예측하게 만드는 것입니다.

이러한 방법에서 LM 목적을 사용하면서 미래의 단어의 연결을 유지할 수 있습니다.각 단계에서 모든 단어 토큰의 15%를 무작위로 예측합니다. 이 중 80%는 mask 토큰으로 대체되며 10%는 무작위 토큰, 그리고 10%는 변하지 않습니다.

게다가 masked word는 fine-tuning 단계에서 보여지지 않기때문에 모델은 complacent를 얻지 못하고 non-masekd 단어의 강한 표현에 의존하게 됩니다. 초기에 BERT는 추가적인 하나의 sentence가 다른 sentece들을 따르는지 아닌지의 추가적인 목적으로 다음 문장 예측이라는 목적을 가졌습니다. 그러나 insignificant effect를 가지기 때문에 나중에 제거되었습니다.

Ⅰ. Introduction

사전 학습된 언어 표현을 down-stream task에 적용하는 두가지 전략으로 feature-based와 fine-tunning이 존재하고, 두 모델은 단방향 언어 모델로 일반적인 언어 표현을 학습합니다.

fine-tuning 접근법에서 사전학습된 표현의 힘을 논의하고, 단방향 언어 모델의 단점을 MLM(masked language model)을 사용하여 보완합니다.

Ⅱ. BERT

1 Figure 1 : overall BERT, Figure 1에서 보여지듯 입력 임베딩을 E로 final hidden 벡터르의 특별한 [CLS] 토큰을 C로 그리고 입력 토큰의 마지막 final hidden vector를 T로.

BERT의 주요 특징은 통일된 아키텍쳐로 사전 학습과 down-stream 아키텍처 사이의 근소한 차이만 있다는 것입니다.

pre-training과 fine-tuning의 단계가 존재합니다.

  • pre-training

    unlabeled 데이터를 사용하여 다양한 사전학습 작업을 수행합니다.

  • fine-tuning

    위의 사전학습된 파라미터를 초기 파라미터로 설정하고 down-stream 작업에서 labeled 데이터를 사용하여 파인튜닝합니다.

ⅰ. Input/output representation

다양한 down-stream task를 다루는 모델로 만들기 위해서 입력 표현은 하나의 token sequence으로 표현 가능해야합니다.

2 Figure 2: BERT의 입력 표현, 토큰이 주어졌을때 입력표현은 상호작용하는 토큰, segment, position embedding을 합치는 것으로 구축됩니다.

모든 시퀀스들은 특별한 토큰([CLS])로 시작하고, pair를 이루는 문장(QA 등)은 [SEP] 라는 토큰을 활용해 나뉩니다.

Figure 1에서 보이듯이 [CLS] 토큰의 마지막 hidden state(Figure 1의 C)에서는 해당 문장의 정보를 요약한 표현으로 사용되어 분류작업에 사용됩니다.

ⅱ. pre-training BERT

BERT를 두 비지도 작업을 사용하여 사전학습한다.

  • MLM(Masked LM)

    입력 토큰의 일정 비율을 무작위로 마스킹하고 그 마스킹 된 토큰을 예측합니다.

    이는 Standard LM과 유사한 방식으로 Mask token과 상호작용하는 최종 은닉 벡터는 단어장의 대한 softmax로 넘어가게 됩니다. Denoising AE와 달리 전체 입력을 재구축하는것 대신 가려진 단어만 예측합니다.

    사전학습동안 [MASK] 토큰은 나타나지 않기 때문에 사전학습과 파인튜닝에서의 불일치를 만들게 됩니다. 이를 완화하기 위해 15%의 토큰 위치를 선택하고, 아래와 같이 확률적으로 가려진 토큰을 대체합니다.

    • 80% : [MASK] 토큰으로 대체
    • 10% : 무작위 토큰으로 대체
    • 10% : 현재 토큰 유지
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    def create_masked_lm_predictions(...):
        
      ...
    
      masked_token = None
      # [MASK] 토큰 대체
      if rng.random() < 0.8:
        masked_token = "[MASK]"
      else:
        # 현재 토큰 유지
        if rng.random() < 0.5:
          masked_token = tokens[index]
        # 무작위 토큰 대체
        else:
          masked_token = vocab_words[rng.randint(0, len(vocab_words) - 1)]
    
      ... 중략
    
    
  • NSP(Next sentence prediction)

    QA, NLI(자연어 추론) 등과 같은 downstream 작업들은 두 문장 사이의 관계를 이해하는 것에 기반하지만 언어 모델링으로는 직접적인 수집이 되지 못합니다.

    문장 관계를 이해하는 모델을 학습시키기 위해 이진화된 다음 문장 예측 작업을 사전학습 합니다.

    문장 관계를 이해하는 모델을 학습하기위해 binarized 된 다음 문장 예측 작업을 위해 사전학습한다. 단일언어를 사용하는\(_{monolingual}\) 데이터에서 쉽게 생성할 수 있습니다.

    사전학습 분류시 A, B를 아래와 같이 설정하여 학습합니다.

    • 50 % : IsNext, 실제 문장
    • 50 % : NotNext, 무작위 문장

    이를 통해 BERT는 모든 파라미터를 다운스트림 작업의 파라미터로 초기화하는데 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def create_instances_from_document(...):

  ...

  # NotNext
  is_random_next = False
  if len(current_chunk) == 1 or rng.random() < 0.5:
    is_random_next = True
    target_b_length = target_seq_length - len(tokens_a)

    for _ in range(10):
      random_document_index = rng.randint(0, len(all_documents) - 1)
      if random_document_index != document_index:
        break

    random_document = all_documents[random_document_index]
    random_start = rng.randint(0, len(random_document) - 1)
    for j in range(random_start, len(random_document)):
      tokens_b.extend(random_document[j])
      if len(tokens_b) >= target_b_length:
        break

    num_unused_segments = len(current_chunk) - a_end
    i -= num_unused_segments

  # IsNext
  else:
    is_random_next = False
    for j in range(a_end, len(current_chunk)):
      tokens_b.extend(current_chunk[j])
  truncate_seq_pair(tokens_a, tokens_b, max_num_tokens, rng)

  ...

ⅲ. fine-tuning bert

BERT는 셀프 어텐션 매커니즘을 통해 일반적으로 텍스트 쌍을 독립적으로 인코딩한 다음 양방향 교차 어텐션을 적용하는 두 단계를 통합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def attention_layer(from_tensor,
                    to_tensor,
                    attention_mask=None,
                    num_attention_heads=1,
                    size_per_head=512,
                    query_act=None,
                    key_act=None,
                    value_act=None,
                    attention_probs_dropout_prob=0.0,
                    initializer_range=0.02,
                    do_return_2d_tensor=False,
                    batch_size=None,
                    from_seq_length=None,
                    to_seq_length=None):
  """Performs multi-headed attention from `from_tensor` to `to_tensor`.

  This is an implementation of multi-headed attention based on "Attention
  is all you Need". If `from_tensor` and `to_tensor` are the same, then
  this is self-attention. Each timestep in `from_tensor` attends to the
  corresponding sequence in `to_tensor`, and returns a fixed-with vector.

  This function first projects `from_tensor` into a "query" tensor and
  `to_tensor` into "key" and "value" tensors. These are (effectively) a list
  of tensors of length `num_attention_heads`, where each tensor is of shape
  [batch_size, seq_length, size_per_head].

  Then, the query and key tensors are dot-producted and scaled. These are
  softmaxed to obtain attention probabilities. The value tensors are then
  interpolated by these probabilities, then concatenated back to a single
  tensor and returned.

  In practice, the multi-headed attention are done with transposes and
  reshapes rather than actual separate tensors.

  Args:
    from_tensor: float Tensor of shape [batch_size, from_seq_length,
      from_width].
    to_tensor: float Tensor of shape [batch_size, to_seq_length, to_width].
    attention_mask: (optional) int32 Tensor of shape [batch_size,
      from_seq_length, to_seq_length]. The values should be 1 or 0. The
      attention scores will effectively be set to -infinity for any positions in
      the mask that are 0, and will be unchanged for positions that are 1.
    num_attention_heads: int. Number of attention heads.
    size_per_head: int. Size of each attention head.
    query_act: (optional) Activation function for the query transform.
    key_act: (optional) Activation function for the key transform.
    value_act: (optional) Activation function for the value transform.
    attention_probs_dropout_prob: (optional) float. Dropout probability of the
      attention probabilities.
    initializer_range: float. Range of the weight initializer.
    do_return_2d_tensor: bool. If True, the output will be of shape [batch_size
      * from_seq_length, num_attention_heads * size_per_head]. If False, the
      output will be of shape [batch_size, from_seq_length, num_attention_heads
      * size_per_head].
    batch_size: (Optional) int. If the input is 2D, this might be the batch size
      of the 3D version of the `from_tensor` and `to_tensor`.
    from_seq_length: (Optional) If the input is 2D, this might be the seq length
      of the 3D version of the `from_tensor`.
    to_seq_length: (Optional) If the input is 2D, this might be the seq length
      of the 3D version of the `to_tensor`.

  Returns:
    float Tensor of shape [batch_size, from_seq_length,
      num_attention_heads * size_per_head]. (If `do_return_2d_tensor` is
      true, this will be of shape [batch_size * from_seq_length,
      num_attention_heads * size_per_head]).

  Raises:
    ValueError: Any of the arguments or tensor shapes are invalid.
  """

  def transpose_for_scores(input_tensor, batch_size, num_attention_heads,
                           seq_length, width):
    output_tensor = tf.reshape(
        input_tensor, [batch_size, seq_length, num_attention_heads, width])

    output_tensor = tf.transpose(output_tensor, [0, 2, 1, 3])
    return output_tensor

  from_shape = get_shape_list(from_tensor, expected_rank=[2, 3])
  to_shape = get_shape_list(to_tensor, expected_rank=[2, 3])

  if len(from_shape) != len(to_shape):
    raise ValueError(
        "The rank of `from_tensor` must match the rank of `to_tensor`.")

  if len(from_shape) == 3:
    batch_size = from_shape[0]
    from_seq_length = from_shape[1]
    to_seq_length = to_shape[1]
  elif len(from_shape) == 2:
    if (batch_size is None or from_seq_length is None or to_seq_length is None):
      raise ValueError(
          "When passing in rank 2 tensors to attention_layer, the values "
          "for `batch_size`, `from_seq_length`, and `to_seq_length` "
          "must all be specified.")

  # Scalar dimensions referenced here:
  #   B = batch size (number of sequences)
  #   F = `from_tensor` sequence length
  #   T = `to_tensor` sequence length
  #   N = `num_attention_heads`
  #   H = `size_per_head`

  from_tensor_2d = reshape_to_matrix(from_tensor)
  to_tensor_2d = reshape_to_matrix(to_tensor)

  # `query_layer` = [B*F, N*H]
  query_layer = tf.layers.dense(
      from_tensor_2d,
      num_attention_heads * size_per_head,
      activation=query_act,
      name="query",
      kernel_initializer=create_initializer(initializer_range))

  # `key_layer` = [B*T, N*H]
  key_layer = tf.layers.dense(
      to_tensor_2d,
      num_attention_heads * size_per_head,
      activation=key_act,
      name="key",
      kernel_initializer=create_initializer(initializer_range))

  # `value_layer` = [B*T, N*H]
  value_layer = tf.layers.dense(
      to_tensor_2d,
      num_attention_heads * size_per_head,
      activation=value_act,
      name="value",
      kernel_initializer=create_initializer(initializer_range))

  # `query_layer` = [B, N, F, H]
  query_layer = transpose_for_scores(query_layer, batch_size,
                                     num_attention_heads, from_seq_length,
                                     size_per_head)

  # `key_layer` = [B, N, T, H]
  key_layer = transpose_for_scores(key_layer, batch_size, num_attention_heads,
                                   to_seq_length, size_per_head)

  # Take the dot product between "query" and "key" to get the raw
  # attention scores.
  # `attention_scores` = [B, N, F, T]
  attention_scores = tf.matmul(query_layer, key_layer, transpose_b=True)
  attention_scores = tf.multiply(attention_scores,
                                 1.0 / math.sqrt(float(size_per_head)))

  if attention_mask is not None:
    # `attention_mask` = [B, 1, F, T]
    attention_mask = tf.expand_dims(attention_mask, axis=[1])

    # Since attention_mask is 1.0 for positions we want to attend and 0.0 for
    # masked positions, this operation will create a tensor which is 0.0 for
    # positions we want to attend and -10000.0 for masked positions.
    adder = (1.0 - tf.cast(attention_mask, tf.float32)) * -10000.0

    # Since we are adding it to the raw scores before the softmax, this is
    # effectively the same as removing these entirely.
    attention_scores += adder

  # Normalize the attention scores to probabilities.
  # `attention_probs` = [B, N, F, T]
  attention_probs = tf.nn.softmax(attention_scores)

  # This is actually dropping out entire tokens to attend to, which might
  # seem a bit unusual, but is taken from the original Transformer paper.
  attention_probs = dropout(attention_probs, attention_probs_dropout_prob)

  # `value_layer` = [B, T, N, H]
  value_layer = tf.reshape(
      value_layer,
      [batch_size, to_seq_length, num_attention_heads, size_per_head])

  # `value_layer` = [B, N, T, H]
  value_layer = tf.transpose(value_layer, [0, 2, 1, 3])

  # `context_layer` = [B, N, F, H]
  context_layer = tf.matmul(attention_probs, value_layer)

  # `context_layer` = [B, F, N, H]
  context_layer = tf.transpose(context_layer, [0, 2, 1, 3])

  if do_return_2d_tensor:
    # `context_layer` = [B*F, N*H]
    context_layer = tf.reshape(
        context_layer,
        [batch_size * from_seq_length, num_attention_heads * size_per_head])
  else:
    # `context_layer` = [B, F, N*H]
    context_layer = tf.reshape(
        context_layer,
        [batch_size, from_seq_length, num_attention_heads * size_per_head])

  return context_layer

작업에 대한 입력과 출력을 연결하여 모든 파라미터를 end-to-end로 파인튜닝합니다. QA 작업을 예시로 입력에서 사전 훈련에서의 A, B문장은 Question, Paragraph로 대체하고, 출력에서 토큰 표현은 Token-Level 작업의 출력층으로 들어갑니다.

이러한 BERT는 GLUE1, SQuAD2, SWAG3 등 다양한 데이터셋에서 우수한 성능을 보였습니다.

Ⅲ. REFERENCES

  1. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
  2. PYTORCH-TRANSFORMERS
  3. google-research bert github



  1. General Language Understanding Evaluation benchmark 

  2. The Stanford Question ANswering Dataset Q/A 짝 

  3. The Situations With Adversarial Generations dataset 

This post is licensed under CC BY 4.0 by the author.