2023. 8. 24. 14:49ㆍ연구 프로젝트/강화학습 챗봇
※어디까지나 도전기일 뿐, 맞는 방법이 아니므로 따라하지 마시오.※
1. 원본 코드
1) 코드 출처
내가 참고하는 논문의 코드가 처음부터 끝까지 다 있다...!
GitHub - Ls-Dai/Deep-Reinforcement-Learning-for-Dialogue-Generation-in-PyTorch
Contribute to Ls-Dai/Deep-Reinforcement-Learning-for-Dialogue-Generation-in-PyTorch development by creating an account on GitHub.
github.com
2) 코드 작동 과정
*코드 알고리즘 작동 과정을 간단하게 시각화
-원 논문에서 사용한 모델: Seq2seq
-모델 학습 과정: 지도 학습을 이용해 모델 첫 학습 -> 강화학습을 이용해 추가적인 학습 진행
-강화학습의 보상 기준 3가지: 답변 난이도, 발화자의 답변 반복성, 전체 대화의 맥락 및 문법
2. 강화학습의 보상 기준 코드 수정
1) 답변의 난이도
*챗봇이 반환하는 질문에 대해 대답하기 쉬운지 측정
-"I don't know"와 같은 dull response를 반환할 확률을 이용해 계산
-S: list of dull responses consisting 8 turns
-NS: cardinality of S
-Ns: the number of tokens in the dull response s
-pseq2seq: likelihood output by Seq2seq models (learned based on the MLE objective)
(+) target S의 길이로 scaling
*원본 코드
def ease_of_answering(input_variable, lengths, dull_responses, mask, max_target_len, encoder, decoder, batch_size, teacher_forcing_ratio):
NS=len(dull_responses)
r1=0
for d in dull_responses:
d, mask, max_target_len = output_var(d, voc)
newD, newMask = transform_tensor_to_same_shape_as(d, input_variable.size())
#tar, mask, max_target_len = convertTarget(d)
forward_loss, forward_len, _ = rl(input_variable, lengths, newD, newMask, max_target_len, encoder, decoder, batch_size, teacher_forcing_ratio)
# log (1/P(a|s)) = CE --> log(P(a | s)) = - CE
if forward_len > 0:
r1 -= forward_loss / forward_len
if len(dull_responses) > 0:
r1 = r1 / NS
return r1
-dull responses 리스트의 길이(요소 개수)를 NS에 저장 후, dull responses 내 각 response에 대해 반복문 실행
-각 dull response를 입력값으로 하여, input_variable(입력값, 하나의 발화)과 똑같은 크기의 새로운 마스크 텐서 및 dull response 텐서 생성
-input_variable(수식에서 a)이 주어졌을 때, 각각의 dull response(수식에서는 소문자 s)를 반환할 확률에 따른 손실값 계산
*수정 코드
dull_responses = ["뭘 말씀하시는지 모르겠어요", "뭘 말하는지 모르겠어", "저는 몰라요", "나는 몰라", "몰라요", "몰라",
"뭘 말하는지 모르겠어요", "이해하지 못했어요", "이해하지 못했어", "이해가 안 돼요", "이해가 안 돼",
"아무것도 몰라요", "아무것도 몰라", "모르겠어요", "모르겠어", "무슨 말", "무슨 말씀"]
def ease_of_answering(token_ids, mask, labels_ids, dull_responses, forward_model, forward_optimizer, criterion):
NS = len(dull_responses)
r1 = 0
dataset = ChatbotDataset(dull_responses)
dataloader = DataLoader(train_set, batch_size=32, num_workers=0, shuffle=True, collate_fn=collate_batch)
for d in dataloader:
newD, newMask, newLabel = d
newD = newD.to(device)
newMask = newMask.to(device)
newLabel = newLabel.to(device)
forward_loss, _ = RL(token_ids, newMask, newD, forward_model, forward_optimizer, criterion)
forward_len = len(newD)
# log (1/P(a|s)) = CE --> log(P(a | s)) = - CE
if forward_len > 0:
r1 -= forward_loss / forward_len
if len(dull_responses) > 0:
r1 = r1 / NS
return r1
-dull responses를 한국어 리스트로 변경
-dull response 내 답변들 역시 입력값과 똑같은 형태인 ChatbotDataset 클래스 형태로 저장 및 DataLoader 객체 생성
-dataloader에 있는 각 dull response에 대해 입력값과 똑같은 방식으로 마스크 텐서, 정답값 텐서를 구함
(※정답값 텐서: 손실값을 구할 때 쓰일 정답)
-RL 함수를 이용해 손실값 계산 후, 이를 dull response 개수로 나눠 정규화
2) 새로운 정보 반환
*반복되는 대화 및 응답을 피하기 위해 각 에이전트가 매 턴마다 얼마만큼 새로운 정보(발화)를 반환하는가
-hpi, hpi+1, pi, pi+1: 연속된 두 대화 턴을 입력으로 한 인코더 값
*원본 코드
def information_flow(responses):
r2=0
if(len(responses) > 2):
#2 representations obtained from the encoder for two consecutive turns pi and pi+1
h_pi = responses[-3]
h_pi1 = responses[-1]
# length of the two vector might not match
min_length = min(len(h_pi), len(h_pi+1))
h_pi = h_pi[:min_length]
h_pi1 = h_pi1[:min_length]
#cosine similarity
#cos_sim = 1 - distance.cosine(h_pi, h_pi1)
cos_sim = 1 - distance.cdist(h_pi.cpu().numpy(), h_pi1.cpu().numpy(), 'cosine')
#Handle negative cos_sim
if np.any(cos_sim <= 0):
r2 = - cos_sim
else:
r2 = - np.log(cos_sim)
r2 = np.mean(r2)
return r2
-각 에이전트의 발화마다 코사인 유사도 값 계산
-사용자의 발화끼리, 챗봇의 발화끼리, 현재 답변과 이전 턴의 답변에 대해 코사인 유사도값(위 코드에서는 cos_sim)을 구함: : 코사인 유사도값은 -1과 1 사이의 값을 가질 수 있으며, 절대값이 0에 가까울수록 두 요소(벡터)는 유사하지 않고, 절대값이 1에 가까울수록 두 요소는 유사하다고 볼 수 있음. 그러나 -1에 가까워지는 경우는 두 요소의 방향이 다른 경우
-보상 값 = -(negative log of the cosine similarity)
-단, cos_sim 요소 중 0 혹은 음수가 하나라도 있는 경우: 로그 함수는 음수 값을 입력값으로 받을 수 없기에 이런 경우는 로그함수를 적용하지 않고 부호만 변경
-코사인 유사도 값의 절댓값이 커질수록 r2, 보상 값은 작아지는 구조
*수정 코드
(지금은 원본 코드와 동일)
3) 대화 문맥의 일정함
*보상 점수는 높게 책정되나 맥락에 맞지 않거나 문법에 맞지 않는 답변을 반환하는 상황을 막고 답변의 정확성 향상
-pseq2seq(a | qi, pi): 이전 발화 [pi, qi]가 주어졌을 때 답변 a가 반환될 확률
-pbackwardseq2seq(qi | a): 답변 a가 주어졌을 때 qi가 반환될 backward 확률
*원본 코드
def semantic_coherence(input_variable, lengths, target_variable, mask, max_target_len, forward_encoder, forward_decoder, backward_encoder, backward_decoder, batch_size, teacher_forcing_ratio):
#print("semantic_coherence-IN R3:")
#print("semantic_coherence-Input_variable.shape :", input_variable.shape)
#print("semantic_coherence-Lengths.shape :", lengths.shape)
#print("semantic_coherence-Target_variable.shape :", target_variable.shape)
#print("semantic_coherence-Mask.shape :", mask.shape)
#print("semantic_coherence-Max_Target_Len :", max_target_len)
r3 = 0
forward_loss, forward_len, _ = rl(input_variable, lengths, target_variable, mask, max_target_len, forward_encoder, forward_decoder, batch_size, teacher_forcing_ratio)
ep_input, lengths_trans = convert_response(target_variable, batch_size)
#print("semantic_coherence-ep_input.shape :", ep_input.shape)
#print("semantic_coherenceLengths transformed.shape :", lengths_trans.shape)
ep_target, mask_trans, max_target_len_trans = convert_target(target_variable, batch_size)
#print("semantic_coherence-ep_target.shsape :", ep_target.shape)
#print("semantic_coherence-mask transformed.shape :", mask_trans.shape)
#print("semantic_coherence-max_target_len_trans.shape :", max_target_len_trans)
backward_loss, backward_len, _ = rl(ep_input, lengths_trans, ep_target, mask_trans, max_target_len_trans, backward_encoder, backward_decoder, batch_size, teacher_forcing_ratio)
if forward_len > 0:
r3 += forward_loss / forward_len
if backward_len > 0:
r3+= backward_loss / backward_len
return r3
-rl 함수(이는 다음 글에서 설명): 간단하게만 적으면...손실값 계산 및 챗봇이 생성한 답변을 반환하는 함수
-input_variable: 입력값, target_variable: 출력값
-input_variable이 주어졌을 때 target_variable이 나올 확률로 손실 값 계산 → forward loss
-target_variable이 주어졌을 때 input_variable이 나올 확률로 손실 값 계산 → backward loss
-backward_loss를 구할 때 mask 텐서를 그에 맞게 변형
*수정 코드
def semantic_coherence(token_ids, mask, labels_ids, dull_responses, forward_model, forward_optimizer, backward_model, backward_optimizer, criterion, max_len=64):
r3 = 0
forward_loss, _ = RL(token_ids, mask, labels_ids, forward_model, forward_optimizer, criterion)
#need mask of labels_ids
backward_loss, _ = RL(labels_ids, mask, token_ids, forward_model, forward_optimizer, criterion)
mask = [0] * len(labels_ids) + [1] * len(token_ids) + [0] * (max_len - len(labels_ids) - len(token_ids))
mask = np.array(mask)
if len(token_ids) > 0:
r3 += forward_loss / len(token_ids)
if len(labels_ids) > 0:
r3 += backward_loss / len(labels_ids)
return r3
-forward loss 계산, labels_ids의 mask 텐서를 생성 및 backward loss 계산
(하지만 이러면 안 되었다...왜냐하면 내가 갖고 있는 코드로는 token_ids와 labels_ids 모두 이미 패딩을 완료한 상태이기에 내가 지정한 최대 길이를 갖고 있기 때문이다. 그렇기에 mask 텐서를 저렇게 구해서도 안 된다. 그러나 이때는 아직 코드에 대한 완전한 이해가 되지 않은 상태였기에 그냥 넘어갔다...)
4) 강화학습의 최종 보상
-λ1 + λ2 + λ3 = 1이 되도록 설정
-참고 논문에서는 각각 0.25, 0.25, 0.5로 설정
*보상 계산 함수 원본 코드
def calculate_rewards(input_var, lengths, target_var, mask, max_target_len, forward_encoder, forward_decoder, backward_encoder, backward_decoder, batch_size, teacher_forcing_ratio):
#rewards per episode
ep_rewards = []
#indice of current episode
ep_num = 1
#list of responses
responses = []
#input of current episode
ep_input = input_var
#target of current episode
ep_target = target_var
#ep_num bounded -> to redefine (MEDIUM POST)
while (ep_num <= 10):
print(ep_num)
#generate current response with the forward model
_, _, curr_response = rl(ep_input, lengths, ep_target, mask, max_target_len, forward_encoder, forward_decoder, batch_size, teacher_forcing_ratio)
#Break if :
# 1 -> dull response
# 2 -> response is less than MIN_LENGTH
# 3 -> repetition ie curr_response in responses
if(len(curr_response) < MIN_COUNT):# or (curr_response in dull_responses) or (curr_response in responses)):
break
#Ease of answering
r1 = ease_of_answering(ep_input, lengths, dull_responses, mask, max_target_len, forward_encoder, forward_decoder, batch_size, teacher_forcing_ratio)
#Information flow
r2 = information_flow(responses)
#Semantic coherence
r3 = semantic_coherence(ep_input, lengths, target_var, mask, max_target_len, forward_encoder, forward_decoder, backward_encoder, backward_decoder, batch_size, teacher_forcing_ratio)
#Final reward as a weighted sum of rewards
r = l1*r1 + l2*r2 + l3*r3
#Add the current reward to the list
ep_rewards.append(r.detach().cpu().numpy())
#We can add the response to responses list
curr_response, lengths = convert_response(curr_response, batch_size)
curr_response = curr_response.to(device)
responses.append(curr_response)
#Next input is the current response
ep_input = curr_response
#Next target -> dummy
ep_target = torch.zeros(MAX_LENGTH,batch_size,dtype=torch.int64)
#ep_target = torch.LongTensor(torch.LongTensor([0] * MAX_LENGTH)).view(-1, 1)
ep_target = ep_target.to(device)
#Turn off the teacher forcing after first iteration -> dummy target
teacher_forcing_ratio = 0
ep_num +=1
#Take the mean of the episodic rewards
return np.mean(ep_rewards) if len(ep_rewards) > 0 else 0
-총 10번의 에피소드를 반복
-1번의 에피소드마다 rl 함수를 호출해 Seq2seq 모델로 문장 생성. 단, 해당 문장의 길이가 따로 지정한 최소 길이보다 작으면 중단
-가중합으로 보상 값을 계산한 후, 다음 반복의 입력값을 현재 출력값으로 변경 (이렇게 되면 챗봇은 최대 10번, 자기 스스로와 대화를 진행하게 된다(...))
*수정 코드
(지금은 원본 코드와 동일)
'연구 프로젝트 > 강화학습 챗봇' 카테고리의 다른 글
[Offline 강화학습 챗봇] Policy Gradient를 이용한 구현 도전기 - KoGPT2 Fine-tuning (3) (0) | 2023.08.30 |
---|---|
[Offline 강화학습 챗봇] Policy Gradient를 이용한 구현 도전기 - 강화학습 (2) (2) | 2023.08.24 |
[Offline 강화학습 챗봇] Policy Gradient를 이용한 구현 도전기 - KoGPT2 Fine-tuning (2) (0) | 2023.08.20 |
[Offline 강화학습 챗봇] Policy Gradient를 이용한 구현 도전기 - KoGPT2 Fine-tuning (1) (0) | 2023.08.20 |
[강화학습] Q-Learning vs. Policy Gradient Method (0) | 2023.07.20 |