BERT(Bidirectional Encoder Representations from Transformers) 구현하기 (2/2)
BERT(Bidirectional Encoder Representations from Transformers) 구현하기 (1/2)에서 포스팅된 내용을 기반으로 Naver 영화리뷰 감정분석 학습과정을 정리 하겠습니다.
Naver 영화리뷰 감정분석은 Classification task로 위 그림과 같이 ‘[CLS]’ 필드를 이용합니다.
이 포스트는 BERT 모델 구현에 대한 설명 입니다. 논문에 대한 내용은 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding 논문을 참고 하거나 다른 블로그를 참고 하세요.
미리 확인해야할 포스트
- Sentencepiece를 활용해 Vocab 만들기
- Naver 영화리뷰 감정분석 데이터 전처리 하기
- Transformer (Attention Is All You Need) 구현하기 (1/3)
- Transformer (Attention Is All You Need) 구현하기 (2/3)
- Transformer (Attention Is All You Need) 구현하기 (3/3)
- BERT(Bidirectional Encoder Representations from Transformers) 구현하기 (1/2)
1. Model
BERT(Bidirectional Encoder Representations from Transformers) 구현하기 (1/2)의 BERT 클래스를 이용하여 Naver 영화리뷰 감정분석 분류 모델 클래스를 아래와 같이 정의 합니다.
- inputs, segments를 입력으로 BERT 모델을 실행 합니다. (줄: 13)
- 1변 결과의 outputs_cls 값을 Classification을 위한 값으로 사용합니다. (줄: 15)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
""" naver movie classfication """
class MovieClassification(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.bert = BERT(self.config)
# classfier
self.projection_cls = nn.Linear(self.config.d_hidn, self.config.n_output, bias=False)
def forward(self, inputs, segments):
# (bs, n_enc_seq, d_hidn), (bs, d_hidn), [(bs, n_head, n_enc_seq, n_enc_seq)]
outputs, outputs_cls, attn_probs = self.bert(inputs, segments)
# (bs, n_output)
logits_cls = self.projection_cls(outputs_cls)
# (bs, n_output), [(bs, n_head, n_enc_seq, n_enc_seq)]
return logits_cls, attn_probs
2. DataSet
DataSet
Naver 영화리뷰 감정분석 데이터 셋 입니다.
- 입력 파일로 부터 ‘label’을 읽어 들입니다. (줄: 17)
- 입력 파일로 부터 ‘doc’ token을 읽어 숫자(token id)로 변경 합니다. (줄: 18, 19)
위 그림과 같이 시작은 ‘[CLS]’ 끝은 ‘[SEP]’가 되도록 합니다. - segment(0)를 생성합니다. (줄: 20)
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
""" 영화 분류 데이터셋 """
class MovieDataSet(torch.utils.data.Dataset):
def __init__(self, vocab, infile):
self.vocab = vocab
self.labels = []
self.sentences = []
self.segments = []
line_cnt = 0
with open(infile, "r") as f:
for line in f:
line_cnt += 1
with open(infile, "r") as f:
for i, line in enumerate(tqdm(f, total=line_cnt, desc="Loading Dataset", unit=" lines")):
data = json.loads(line)
self.labels.append(data["label"])
sentence = [vocab.piece_to_id("[CLS]")] + [vocab.piece_to_id(p) for p in data["doc"]] + [vocab.piece_to_id("[SEP]")]
self.sentences.append(sentence)
self.segments.append([0] * len(sentence))
def __len__(self):
assert len(self.labels) == len(self.sentences)
assert len(self.labels) == len(self.segments)
return len(self.labels)
def __getitem__(self, item):
return (torch.tensor(self.labels[item]),
torch.tensor(self.sentences[item]),
torch.tensor(self.segments[item]))
collate_fn
배치단위로 데이터 처리를 위한 collate_fn 입니다.
- inputs의 길이가 같아지도록 짧은 문장에 padding(0)을 추가 합니다. (줄: 5)
padding은 Sentencepiece를 활용해 Vocab 만들기에서 ‘–pad_id=0’옵션으로 지정한 값 입니다. - segments의 길이가 같아지도록 짧은 문장에 padding(0)을 추가 합니다. (줄: 6)
- Label은 길이가 1 고정이므로 stack 함수를 이용해 tensor로 만듭니다. (줄: 9)
1
2
3
4
5
6
7
8
9
10
11
12
13
""" movie data collate_fn """
def movie_collate_fn(inputs):
labels, inputs, segments = list(zip(*inputs))
inputs = torch.nn.utils.rnn.pad_sequence(inputs, batch_first=True, padding_value=0)
segments = torch.nn.utils.rnn.pad_sequence(segments, batch_first=True, padding_value=0)
batch = [
torch.stack(labels, dim=0),
inputs,
segments,
]
return batch
DataLoader
위에서 정의한 DataSet과 collate_fn을 이용해 학습용(train_loader), 평가용(test_loader) DataLoader를 만듭니다.
1
2
3
4
5
6
""" 데이터 로더 """
batch_size = 128
train_dataset = MovieDataSet(vocab, f"{data_dir}/ratings_train.json")
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=movie_collate_fn)
test_dataset = MovieDataSet(vocab, f"{data_dir}/ratings_test.json")
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=False, collate_fn=movie_collate_fn)
3. Evaluate
학습된 MovieClassification 모델의 성능을 평가하기 위한 함수 입니다. 평가는 정확도(accuracy)를 사용 했습니다.
- inputs, segments를 입력으로 MovieClassification을 실행합니다. (줄: 12)
- 1번의 결과 중 첫번째 값이 예측 logits 입니다. (줄: 13)
- logits의 최대값의 index를 구합니다. (줄: 14)
- 3번에게 구한 값과 labels의 값이 같은지 비교 합니다. (줄: 16)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
""" 모델 epoch 평가 """
def eval_epoch(config, model, data_loader):
matchs = []
model.eval()
n_word_total = 0
n_correct_total = 0
with tqdm(total=len(data_loader), desc=f"Valid") as pbar:
for i, value in enumerate(data_loader):
labels, inputs, segments = map(lambda v: v.to(config.device), value)
outputs = model(inputs, segments)
logits_cls = outputs[0]
_, indices = logits_cls.max(1)
match = torch.eq(indices, labels).detach()
matchs.extend(match.cpu())
accuracy = np.sum(matchs) / len(matchs) if 0 < len(matchs) else 0
pbar.update(1)
pbar.set_postfix_str(f"Acc: {accuracy:.3f}")
return np.sum(matchs) / len(matchs) if 0 < len(matchs) else 0
4. Train
MovieClassification 모델을 학습하기 위한 함수 입니다.
- inputs, segments를 입력으로 MovieClassification을 실행합니다. (줄: 11)
- 1번의 결과 중 첫 번째 값이 예측 logits 입니다. (줄: 12)
- logits 값과 labels의 값을 이용해 Loss를 계산합니다. (줄: 14)
- loss, optimizer를 이용해 학습합니다. (줄: 20, 21)
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
""" 모델 epoch 학습 """
def train_epoch(config, epoch, model, criterion_cls, optimizer, train_loader):
losses = []
model.train()
with tqdm(total=len(train_loader), desc=f"Train({epoch})") as pbar:
for i, value in enumerate(train_loader):
labels, inputs, segments = map(lambda v: v.to(config.device), value)
optimizer.zero_grad()
outputs = model(inputs, segments)
logits_cls = outputs[0]
loss_cls = criterion_cls(logits_cls, labels)
loss = loss_cls
loss_val = loss_cls.item()
losses.append(loss_val)
loss.backward()
optimizer.step()
pbar.update(1)
pbar.set_postfix_str(f"Loss: {loss_val:.3f} ({np.mean(losses):.3f})")
return np.mean(losses)
학습을 위한 추가적인 내용을 선언 합니다.
- GPU 사용 여부를 확인합니다. (줄: 1)
- 출력 값 개수를 정의 합니다. (부정(0), 긍정(1) 2가지입니다.) (줄: 2)
- learning_rate 및 학습 epoch를 선언 합니다. (줄: 5, 6)
1
2
3
4
5
6
config.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
config.n_output = 2
print(config)
learning_rate = 5e-5
n_epoch = 10
출력 결과입니다.
1
{'n_enc_vocab': 8007, 'n_enc_seq': 256, 'n_seg_type': 2, 'n_layer': 6, 'd_hidn': 256, 'i_pad': 0, 'd_ff': 1024, 'n_head': 4, 'd_head': 64, 'dropout': 0.1, 'layer_norm_epsilon': 1e-12, 'device': device(type='cuda'), 'n_output': 2}
위에서 선언된 내용을 이용해 학습을 실행하는 함수입니다.
- MovieClassification이 GPU 또는 CPU를 지원하도록 합니다. (줄: 1)
- loss 함수를 선언 합니다. (줄: 4)
- optimizer를 선언 합니다. (줄: 5)
- 각 epoch 마다 학습을 합니다. (줄: 10)
- 각 epoch 마다 평가를 합니다. (줄: 11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def train(model):
model.to(config.device)
criterion_cls = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
best_epoch, best_loss, best_score = 0, 0, 0
losses, scores = [], []
for epoch in range(n_epoch):
loss = train_epoch(config, epoch, model, criterion_cls, optimizer, train_loader)
score = eval_epoch(config, model, test_loader)
losses.append(loss)
scores.append(score)
if best_score < score:
best_epoch, best_loss, best_score = epoch, loss, score
print(f">>>> epoch={best_epoch}, loss={best_loss:.5f}, socre={best_score:.5f}")
return losses, scores
Train (No Pretrain)
Pretrain을 사용하지 않고 학습을 진행 합니다.
- MovieClassification을 생성합니다. (줄: 1)
- 추가적인 처리 없이 생성된 MovieClassification으로 학습을 진행 합니다. (줄: 3)
1
2
3
model = MovieClassification(config)
losses_00, scores_00 = train(model)
Train (20 epoch Pretrain)
20 epoch Pretrain된 모델을 이용해 학습을 진행 합니다.
- MovieClassification을 생성합니다. (줄: 1)
- BERT(Bidirectional Encoder Representations from Transformers) 구현하기 (1/2)에서 Pretrain 모델을 로드 합니다. (줄: 3, 4)
- MovieClassification으로 학습을 진행 합니다. (줄: 6)
1
2
3
4
5
6
model = MovieClassification(config)
save_pretrain = "<path of data>/save_bert_pretrain.pth"
model.bert.load(save_pretrain)
losses_20, scores_20 = train(model)
5. Result
학습결과 및 평가결과는 아래와 같습니다.
Pretrain을 안한 경우는 정확도(score)가 81.8% 정도 나왔습니다.
Pretrain을 20 epoch 한 경우는 정확도(score)가 81.9% 정도 나왔습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# table
data = {
"loss_00": losses_00,
"socre_00": scores_00,
"loss_20": losses_20,
"socre_20": scores_20,
}
df = pd.DataFrame(data)
display(df)
# graph
plt.figure(figsize=[12, 4])
plt.plot(scores_00, label="score_00")
plt.plot(scores_20, label="score_20")
plt.legend()
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.show()
loss_00 | socre_00 | loss_20 | socre_20 |
---|---|---|---|
0.522093 | 0.783687 | 0.515065 | 0.782307 |
0.436096 | 0.795608 | 0.434717 | 0.798528 |
0.408585 | 0.804388 | 0.405332 | 0.809989 |
0.388935 | 0.810749 | 0.383013 | 0.814149 |
0.371047 | 0.813969 | 0.364083 | 0.816289 |
0.355533 | 0.810529 | 0.347914 | 0.817629 |
0.341048 | 0.815309 | 0.329771 | 0.820329 |
0.324362 | 0.813649 | 0.311193 | 0.823969 |
0.307140 | 0.816129 | 0.295256 | 0.822689 |
0.289369 | 0.817949 | 0.276582 | 0.818469 |
6. 참고
자세한 내용은 다음을 참고 하세요.