[플레이데이터 SK 네트웍스 Family AI 캠프 17기] 10주차 회고
너무 바빠 회고가 밀린 사이 벌써 어느덧 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 사이트로 이를 구현해보고자 합니다.