너무 바빠 회고가 밀린 사이 벌써 어느덧 9월이 되었습니다.
그동안 회고를 꾸준히 작성하지 못한 것을 반성하며,,
9주차 회고를 늦게나마 써보려고 합니다.
앞으로 꾸준히 써보겠습니다 ㅎㅎ
화이팅!
1. 이번 주에 배운 내용 중 인상적이었던 것
(1) 트랜스포머
1. 트랜스포머란?
- 순차 데이터(=시퀀스 데이터, 그 중에서도 특히 자연어)를 처리하기 위한 혁신적인 딥러닝 모델.
- NLP와 컴퓨터 비전 등 다양한 분야에서 사용
- Self-Attention 메커니즘과 병렬 연산을 기반으로 하여 RNN 모델의 단점 보완
(2) 트랜스포머의 가장 큰 특징
2. 트랜스포머의 가장 특징적인 구조
- 인코더 : 자연어 이해하는 역할
- 디코더 : 인코더에서 이해한 정보를 활용해 출력 시퀀스를 형성하는 역할
- 인코더 블럭과 디코더 블럭을 여러개 가지고 있음
(3) 트랜스포머의 인코더
- 인코더 부분 : 입력 시퀀스를 처리하여 컨텍스트 정보 생성
- Positional Encoding : 문장에서 어디에 위치했는지 알게 해주도록 위치 정보 추가해줌. 병렬적으로 단어 정보 처리하기 때문에 위치 정보를 꼭 추가해야함
- Multi-Head Attention : 어느 것을 중요 포인트로 보고 읽을지 결정해서 다각도에서 자연어 해석해 가중치 계산. 서로 다른 시각에서 데이터 바라보게 함.
ex. 문장을 읽을 때, 기관을 중심으로, 시간을 중심으로, 인물을 중심으로 읽어서 한꺼번에 고려
- Self Attention을 병렬적으로 수행하여 다양한 관계 학습
- 미래 단어를 참조하지 않게 마스크 적용
- Add & Normalization : 기울기 소실 문제를 해결하기 위해 잔차연결 해준 것과 유사
- Feed Forward : 신경망으로 연결 . 단어 표현 강화하고 비선형으로 만들어서 더 잘 이해하게 도와줌.
(4) 트랜스포머의 디코더
- 디코더 부분: 인코더의 출력을 받아 최종 출력을 생성
- Ouput Embedding : 앞에서 생성된 맥락이 들어와서 임베딩 처리됨
- Positional Encoding : 위치 정보 추가
- 확률이 높은 걸 뽑아내기 위해 Softmax 통과해서 추론된 결과로 단어 생성
- Self-Attention : 각 단어가 다른 단어들과의 관계를 학습하여 문맥 이해
2. 일주일 동안 고민한 내용
(1) SentencePiece Tokenizer 문장 시작 & 끝 태그 변경
SentencePiece Tokenizer는 문장을 tokenize하면서
문장 앞뒤에 각각 <s> 와 <\s> 태그를 붙인다.
SentencePiece Tokenizer 객체를 생성하면서 tokenize를 하게 되면 vocab 에

이런 식으로 태그가 저장이 된 걸 볼 수 있다.
<s> 와 <\s> 태그를 각각 <bos> 와 <eos>로 바꾸어주고 싶어 cmd를 이렇게 수정해보았으나,
cmd = f'--input={input} --model_prefix={model_prefix} --vocab_size={vocab_size} --user_defined_symbols={"<bos>,<eos>"}'
(user_defined_symbols 에 두 개 이상을 넣어줄 때 하나의 문자열에 콤마로 구분해서 넣어줘야함)
결과가 예상과는 다르게 나왔다.

원래 있던 <s>와 <\s> 태그 밑에 <bos>와 <eos>가 또 생겼다.
bos_id 와 eos_id를 조정해봐도 위와 똑같이 나온다.
이를 통해 bos_id와 eos_id는 각각 <s> 와 <\s> 의 인덱스를 의미한다는 것을 알게 되었다.
즉, bos_id와 eos_id를 변경해줘도 <bos>와 <eos> 인덱스가 바뀌는 것이 아니라,
<s> 와 <\s> 의 인덱스가 변경된다.
이를 해결하기 위해 열심히 알아본 결과,
bos_piece와 eos_piece라는 것을 지정해줄 수 있다는 것을 알게 되었다.
cmd = f'--input={input} --model_prefix={model_prefix} --vocab_size={vocab_size} --bos_id={1} --bos_piece={"<bos>"} --eos_id={2} --eos_piece={"<eos>"} --user_defined_symbols={"<bos>,<eos>"}'
이렇게 cmd 를 입력해주면 다음과 같이 원하는 결과를 얻을 수 있다.

(2) 보이스 챗봇 만들기
이번주 수업에서 gpt api를 이용한 prompt engineering을 배웠다.
또, tts와 stt 도 살짝 맛 보았다.
이번주 배운 것들을 토대로 주제를 하나 정해 보이스 챗봇을 만들어 보았다.
주제: 밸런스 게임 토론을 할 수 있는 챗봇을 만들어보자!
요구사항 : 챗봇의 반응을 보이스로 출력하고, 나의 말도 텍스트가 아닌 보이스로 줄 것!
챗봇 작동 순서
1. 챗봇에게 밸런스 게임을 하자고 제안하면 챗봇이 주제를 3개 반환
2. 챗봇이 말한 주제 3가지 중 하나를 골라 얘기하면서 토론을 시작
3. 챗봇과 이야기를 하다가 그만하고 싶다는 의사를 전달하면 토론 종료
챗봇을 만들 때 가장 신경썼던 부분은
챗봇의 prompt engineering 과 대화 내용을 기억하게 하는 것이었습니다.
[1] prompt engineering 부분
하나의 client 가 주제도 만들고 토론도 진행하면 좋겠지만,
그렇게 할 경우 client에 전달되는 token의 개수가 너무 많아지기 때문에
필요 이상으로 과금되는 것을 막기 위해
주제를 만드는 client와 토론하는 client를 분리해 생성했습니다.
# 주제 만드는 clinet에 필요한 system_instruction 반환
def get_theme_instruction():
system_instruction = '''
당신은 금요일에 퇴근하고 친구들을 만나 맛있는 저녁을 먹고 술을 적당히 마셔 신난 상태입니다.
술자리에서 다같이 재미있게 얘기할 만한 연애 밸런스 게임 주제 세 개를 선정해주세요.
### 지시사항 ###
- 토론이 가능한 주제를 선정할 것
- 성인이니까 선정적인 주제도 하나 정도는 출력할 것
- 선택하기 어렵고 애매한 주제를 선택할 것
- 여사친/남사친 문제를 한 개 이상 포함할 것
### 출력 형식 ###
좋아! 재미있는 주제를 세 가지 생각해봤어.
주제 1: 선택지 vs 선택지.
주제 2: 선택지 vs 선택지.
'''
return system_instruction
# 초기 chat_log 설정하고 반환하는 함수
def get_fisrt_chatlog(system_instruction):
chat_log = [{ 'role': 'system', 'content': [{ 'type': 'text', 'text': system_instruction }] }
]
return chat_log
# 주제를 만들어주는 함수
def make_theme(chat_log, temperature=0.3):
client = OpenAI()
response = client.chat.completions.create(
model='gpt-4o',
messages= chat_log,
response_format={
'type':'text'
},
temperature=temperature,
max_tokens=2048,
top_p = 1,
frequency_penalty=0,
presence_penalty=0
)
return response.choices[0].message.content
개인적으로 밸런스 게임을 했을 때 어렵다고 느꼈던 주제들은
선정적이거나 남사친/여사친에 관련된 주제들이었습니다.
그래서 이를 직접적으로 넣어주었습니다.
# 토론하는 client의 system_instruction 가져오는 함수
def get_discussion_instruction(theme_output):
system_instruction = f'''
당신은 금요일에 퇴근하고 친구들을 만나 맛있는 저녁을 먹고 술을 적당히 마셔 신난 상태입니다.
상대가 재미있는 밸런스 게임을 해보자면서 3가지 주제를 제안했고 이 중 한 가지를 골랐습니다.
{theme_output.split(sep=".")[1:]}
상대가 선정한 주제에 대해 토론해봅시다.
상대가 생각을 바꿀 수 있도록 영악하고 악독하게 비꼬면서 토론을 해주세요.
### 토론시 지시사항 ###
- 3문장 이내로 반말로 대답할 것
- 이해하는 척 하면서 교묘하게 상대가 선택한 선택지에서 악화될 상황을 상상해 말할 것
- 상대와 반대 의견을 주장할 것
- 만약 상대가 '그만하자'라는 말을 하면 "ㅋㅋㅋ 알았어 재밌었다 그치?" 를 그대로 출력할 것
- 예시를 적극 활용할 것
### 주제 묻기 출력 예시 ###
이 중 어떤 주제에 대해 이야기 해볼까?
'''
return system_instruction
# 고른 주제로 토론하게 하는 함수
def discuss(chat_log, temperature=0.3):
client = OpenAI()
response = client.chat.completions.create(
model='gpt-4o',
messages= chat_log,
response_format={
'type':'text'
},
temperature=temperature,
max_tokens=2048,
top_p = 1,
frequency_penalty=1,
presence_penalty=0
)
return response.choices[0].message.content
내가 챗봇이 출력한 세 가지 주제 중 하나 선정하면
토론하는 client가 주제 3개와 내가 고른 주제를 입력받아 이에 대해 토론할 수 있도록 했습니다.
이때, 챗봇이 나를 설득하게 할 수 있도록
비꼬거나 부정적인 상황을 가정해 이야기하도록 했고,
대화하는 것이기 때문에 3문장 이내로 답하도록 지시했습니다.
그리고 이 과정에서 내가 비슷한 말을 반복하더라도
똑같은 값을 출력하지 않도록 penalty를 주었습니다.
그만하자는 뉘앙스로 이야기하면 자연스레 종료되게끔 지시도 해주었습니다.
[2] 대화 내용 기억하기
gpt api를 이용해 client를 만들면 아무리 같은 client 여도 대화내용을 기억하지 못합니다.
그래서 이를 해결하기 위해서 chat_log에 지속적으로
client 응답과 나의 응답이 기록되고 이를 client에 전달할 수 있도록 함수를 만들었습니다.
# chat_log 를 수정하는 함수
def modify_chatlog(chat_log, new_message):
if len(chat_log)%2 ==0:
chat_log.append({ 'role': 'assistant', 'content': [{ 'type': 'text', 'text': new_message }] })
else:
chat_log.append({ 'role': 'user', 'content': [{ 'type': 'text', 'text': new_message }] })
return chat_log
초기 chat_log 에는 'system' 만 존재합니다.
chat_log 의 길이가 짝수일 때에는 'assistant'의 응답이,
길이가 홀수일 때에는 'user'의 응답이 저장될 수 있도록 함수를 만들어
챗봇이나 사용자가 말하는 것들을 계속해서 chat_log에 붙여넣을 수 있도록 했습니다.
그 결과, 이런 식으로 대화를 잘 기억하는 챗봇을 만들 수 있었습니다.


이를 스트림릿으로 구현해보면 어땠을까 하는 아쉬움이 있었습니다.
또, 마음에 들지 않는 주제가 나와도 어쩔 수 없이 출력한 것 중에 골라야한다는 단점이 있었습니다.
그래서 다음주에 이를 발전시켜
주제가 마음에 안 들면 바꾸어달라고 요청할 수 있게 만들어보고,
streamlit 사이트로 이를 구현해보고자 합니다.