
SolarChat 클래스로 Wrapping되어 있으며, 내부적으로 OpenAI API와 호환됩니다.!pip install openai #openai 라이브러리 설치
import pandas as pd
import os
import time
from tqdm import tqdm
from rouge import Rouge # 모델의 성능을 평가하기 위한 라이브러리
from openai import OpenAI # openai==1.2.0
UPSTAGE_API_KEY = "up_****************" # upstage.ai에서 발급받은 API KEY를 입력해주세요.
client = OpenAI(
api_key=UPSTAGE_API_KEY,
base_url="https://api.upstage.ai/v1/solar" #Solar API 엔드포인트
)
엔드포인트란?
Solar Chat API에 요청을 보낼 URL 주소를 뜻함.
https://console.upstage.ai/docs/getting-started
| 지원 모델 이름 | 설명 |
|---|---|
upstage/solar-1-mini-chat | 3B 기반 한국어 최적화 LLM |
upstage/solar-10.7b-instruct-v1.0 | 10.7B 대규모 모델 (Instruct 기반) |
upstage/solar-1-chat | (기본 Chat용 모델) |
# 모델이 만들어내는 말을 실시간으로 글자/단어 단위로 출력하게 되는 구조
stream = client.chat.completions.create(
model = "solar-1-mini-chat",
messages = [
{"role": "system", "content": "You are helpful assistant."},
{"role": "user", "content": "Hello!"}
], #대화 맥락을 구성하는 부분
stream=True,
)
for chunck in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")
🔹 messages는 채팅의 대화 맥락을 구성하는 부분입니다.
🔸 "system" 역할: 모델에게 역할이나 성격을 부여 (ex. "요약 전문가", "친절한 조수")
🔸 "user" 역할: 사용자 입력 ("Hello!")
🔹 stream=True
🔸 응답을 한 번에 받는 대신 단어(또는 토큰) 단위로 실시간 스트리밍 받겠다는 설정입니다.
🔸 stream=False로 하면 전체 문장을 한번에 반환합니다.
🔹 for chunk in stream:
🔸stream 객체는 generator로, 각 chunk는 모델이 토큰 단위로 보내오는 응답입니다.
🔸 for chunk in stream:은 응답이 끝날 때까지 한 줄씩 반복하며 처리합니다.
🔹 if chunk.choice[0].delta.content is not None: 🔸 각 chunk에는 choices리스트가 있으며, 그 안에 **응답된 텍스트 조각**이delta.content`에 저장됩니다.
🔸 이 줄은 응답이 비어 있지 않을 경우만 출력을 하겠다는 조건
🔹 print(chunk.choices[0].delta.content, end="")
🔸 응답된 텍스트 조각을 화면에 출력
🔸end=""는 줄바꿈 없이 이어서 출력하겠다는 뜻
스트리밍으로 모델이 보내는 각 조각(chunk)은 대략 이렇게 생겼어요:
{
"choices": [
{
"delta": {
"content": "안"
},
"finish_reason": null
}
]
}
chunk: 전체 응답 조각 하나chunk.choices: 모델의 응답 리스트 (보통 1개만 존재함)chunk.choices[0]: 첫 번째 응답chunk.choices[0].delta: 새로 생성된 텍스트 조각 (delta는 "차이" 또는 "새로 생성된 부분"이라는 의미)chunk.choices[0].delta.content: 이번에 새로 나온 단어 혹은 글자 (예: "안")chunk
└── choices (list)
└── [0]
└── delta
└── content → 새로 생성된 텍스트 조각 ("안", "녕", ...)
스트리밍에서는 전체 응답이 아닌 "조각(delta)"만 주기 때문에,
한 번에 하나씩 이전 응답 대비 "새로 생성된" 부분만 포함해서 보내는 겁니다.
# 모델 성능에 대한 평가 지표를 정의합니다.본 대회에서는 ROUGE 점수를 통해 모델의 성능을 평가합니다.
rouge = Rouge()
def compute_metrics(pred, gold):
results = rouge.get_scores(pred, gold, avg=True)
#pred(예측요약)과 gold(정답요약)을 비교하여 rouge 전수 계산
result = {key: value["f"] for key, value in results.items()}
#ROUGE-1/2/L 중 F1-score만 뽑아 딕셔너리로 정리
return result #예:{'rouge-1': 0.53, 'rouge-2': 0.39, 'rouge-l': 0.50}형태 리턴
KoNLPy + custom tokenizer 또는 rouge_score (HuggingFace용)를 조합해서 직접 구현해야 함 pred_list = ["요약문1", "요약문2", ...]
gold_list = ["정답1", "정답2", ...]
rouge.get_scores(pred_list, gold_list, avg=True)```
##### ✅ 수정/보완 포인트 (선택 사항)
```python
def compute_metrics(pred, gold):
results = rouge.get_scores(pred, gold, avg=True)
return {
"rouge-1-f": results["rouge-1"]["f"],
"rouge-2-f": results["rouge-2"]["f"],
"rouge-l-f": results["rouge-l"]["f"],
}
이렇게 명시적으로 바꾸면 추후 CSV 저장이나 정렬 시 더 명확하게 활용할 수 있습니다.
# Dialogue를 입력으로 받아, Solar Chat API에 보낼 Prompt를 생성하는 함수를 정의합니다.
def build_prompt(dialogue):
system_prompt = "You are an expert in the field of dialogue summarization. Please summarize the following dialogue."
# system role은 모델에게 역할과 태도를 정의하는 부분.
user_prompt = f"Dialogue:\n{dialogue}\n\nSummary:\n"
#실제 요약할 대화 내용이 들어감.
return [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_prompt
}
]
🔹 user role에는 실제 요약할 대화 내용이 들어갑니다.
- "Dialogue:\n{dialogue}"로 대화가 어떤 것인지 보여주고,
- \n\nSummary:\n으로 요약문을 작성하라고 유도합니다.
- 이 구조는 모델이 "Summary:" 뒤에 이어서 요약을 자연스럽게 생성하도록 합니다.
def summarization(dialogue): #대화 내용(dialogue)을 받아 요약을 수행하는 함수 정의
summary = client.chat.completions.create( #client는 미리 설정된 openai 인스턴스
model="solar-1-mini-chat",
messages=build_prompt(dialogue), #이전에 정의한 함수로 만들어진 프롬프트 메세지 사용
) #openai 호환 방식의 Solar API를 호출
return summary.choices[0].message.content
#최종적으로 요약 결과 텍스트만 반환
| 보완 포인트 | 설명 |
|---|---|
| 🔁 retry logic | API 오류 대비 재시도 로직 (try-except) |
| ⏱ 응답 시간 측정 | inference latency 측정 시 time.time() 사용 |
| 📄 로그 저장 | 출력 로그 또는 실패 대화 저장 |
| 🚫 content null 처리 | choices[0].message.content가 None일 수 있으니 get() 처리 고려 |
def summarization(dialogue):
try:
response = client.chat.completions.create(
model="solar-1-mini-chat",
messages=build_prompt(dialogue),
)
return response.choices[0].message.content.strip()
except Exception as e:
print(f"[Error] summarization failed: {e}")
return "요약 실패"
temperature=0.20.0이면 항상 거의 같은 출력 (가장 확률 높은 토큰만 선택)1.0이면 창의적이고 다양성이 높은 결과0.2는 거의 일관되고 안정적인 요약 결과를 생성하게 합니다.top_p=0.3p 확률에 해당하는 토큰들 중에서 샘플링top_p=0.3이면 → 전체 확률 분포의 상위 30%에 해당하는 토큰 중에서 선택temperature와 함께 모델의 창의성/일관성 trade-off를 세밀하게 조절하는 데 사용됩니다.🔁
temperature와top_p는 일반적으로 하나만 조절하는 게 좋지만, 둘 다 설정해도 동작은 합니다.
(다만 지나치게 조이면 모호하거나 짧은 응답이 나올 수도 있으니 튜닝 필요)
| 목적 | 추천 값 |
|---|---|
| 최대한 일관된 요약 (대회 제출용) | temperature=0.2, top_p=0.3 ✅ |
| 약간의 다양성 | temperature=0.7, top_p=0.9 |
| 다양한 버전 생성 (augmentation) | temperature=1.0, top_p=1.0 |
# Train data 중 처음 3개의 대화를 요약합니다.
def test_on_train_data(num_samples=3):
for idx, row in train_df[:num_samples].iterrows():
dialogue = row['dialogue'] #현재 대화 내용 추출
summary = summarization(dialogue) #정의한 함수통해 solar chat api로 요약 생성
print(f"Dialogue:\n{dialogue}\n")
print(f"Pred Summary: {summary}\n")
print(f"Gold Summary: {row['summary']}\n")
print("=="*50)
.itterows() : 각 행을 (index, row Series) 형태로 반복함.| 개선 포인트 | 설명 |
|---|---|
| ⏱ 속도 측정 | 요약 시간 time.time()으로 측정 |
| 🧪 평가 지표 출력 | compute_metrics(pred, gold) 함수로 ROUGE 점수 함께 출력 |
| 📄 결과 저장 | .csv 또는 .jsonl 파일로 저장하여 나중에 비교 |
| 💻 tqdm progress bar | 10개 이상 처리할 경우 진행률 표시 |
def test_on_train_data(num_samples=3):
for idx, row in train_df[:num_samples].iterrows():
dialogue = row['dialogue']
gold = row['summary']
pred = summarization(dialogue)
scores = compute_metrics(pred, gold)
print(f"Dialogue:\n{dialogue}\n")
print(f"Pred Summary: {pred}\n")
print(f"Gold Summary: {gold}\n")
print(f"ROUGE Scores: {scores}\n")
print("=="*50)
# Validation data의 대화를 요약하고, 점수를 측정합니다.
def validate(num_samples=-1):
# num_samples가 양수일 경우 해당 개수만큼, 음수이면 전체 validation 세트를 사용
val_samples = val_df[:num_samples] if num_samples > 0 else val_df
scores = [] #개별 예측에 대한 점수를 저장할 리스트
for idx, row in tqdm(val_samples.iterrows(), total=len(val_samples)):
dialogue = row['dialogue']
summary = summarization(dialogue)
results = compute_metrics(summary, row['summary'])
# 모델의 요약 결과와 정답 요약 비교하여 ROUGE F1 계산
avg_score = sum(results.values()) / len(results)
# ROUGE-1/2/L의 평균값을 하나의 점수로 계산
scores.append(avg_score)
val_avg_score = sum(scores) / len(scores)
print(f"Validation Average Score: {val_avg_score}")
"val_df의 전체 샘플에 대해 모델이 생성한 요약문과 정답 요약문 사이의 ROUGE-F1 평균 점수" 출력
dialogue summarization은 자유도가 높고 정답이 다양하기 때문에, 일반적인 문서 요약보다 점수가 낮게 나오는 게 일반적입니다.
| 점수 범위 | 의미 |
|---|---|
| 0.05 이하 | 모델이 거의 엉뚱한 요약을 함 (거의 무작위 수준) |
| 0.1~0.2 | 아주 기본적인 의미는 담고 있지만 핵심 정보는 많이 놓침 |
| 0.3~0.5 | 핵심 단어가 많이 일치. 꽤 괜찮은 요약 |
| 0.6 이상 | 거의 정답과 유사한 고성능 요약 모델 수준 |
Validation Average Score: 0.097 ≒ 9.7%
| 가능성 | 설명 |
|---|---|
| 🔹 모델이 대화를 제대로 이해 못함 | prompt가 너무 짧거나 일반적일 수 있음 |
| 🔹 프롬프트가 영어인데 대화가 한글 | 모델 혼란 유발 (한국어 모델이면 system role도 한글로!) |
| 🔹 Solar 모델 성능 한계 | solar-1-mini-chat은 3B 모델로 작은 편 |
| 🔹 gold summary와 표현 방식 차이 | 정답은 "요약적", 모델은 "해석적/묘사적" 생성 |
| 🔹 형태소 단위 ROUGE 미사용 | compute_metrics()가 단어 기준이면 실제 평가보다 점수 낮게 나올 수 있음 |
| 시도할 것 | 설명 |
|---|---|
| ✅ 프롬프트 개선 | system prompt와 user prompt를 모두 한국어로 |
| ✅ 모델 교체 | solar-10.7b-instruct로 전환 (가능하다면) |
| ✅ Few-shot 학습 유도 | 예시 요약문 추가로 성능 향상 유도 |
| ✅ 평가 방식 검토 | rouge_score 또는 형태소 기반 tokenizer 적용 |
""" test 데이터셋에 대해 Solar 모델로 요약을 생성하고,
rate limit(속도 제한)을 고려하여 안전하게 결과 파일로 저장하는 inference 함수"""
def inference():
test_df = pd.read_csv(os.path.join(DATA_PATH, 'test.csv'))
summary = [] #요약 결과를 저장할 리스트summary 초기화
start_time = time.time() #1분 간 처리 개수 측정을 위한 시간 측정 시작
for idx, row in tqdm(test_df.iterrows(), total=len(test_df)):
# test 데이터셋을 한 줄씩 순회하며 tqdm으로 진행률 시각화
dialogue = row['dialogue']
summary.append(summarization(dialogue)) #현재 대화문 요약 후, 리스트에 추가
# Rate limit 방지를 위해 1분 동안 최대 100개의 요청을 보내도록 합니다.
if (idx + 1) % 100 == 0:
end_time = time.time()
elapsed_time = end_time - start_time
#100개 요청마다 경과 시간 측정
if elapsed_time < 60:
wait_time = 60 - elapsed_time + 5
print(f"Elapsed time: {elapsed_time:.2f} sec")
print(f"Waiting for {wait_time} sec")
time.sleep(wait_time)
# 만약 100개를 1분보다 빨리 처리했다면 -> 남은 시간 + 여유 5초만큼 대기
start_time = time.time()
# 다음 100개 구간을 위해 시간 초기화
output = pd.DataFrame(
{
"fname": test_df['fname'],
"summary" : summary,
}
)
# test_df에서 fname열과 생성된 summary리스트를 합쳐 새 DataFrame 생성
if not os.path.exists(RESULT_PATH):
os.makedirs(RESULT_PATH) #결과 저장 폴더가 없으면 생성
output.to_csv(os.path.join(RESULT_PATH, "output_solar.csv"), index=False)
#최종 요약 결과를 output_solar.csv로 저장
return output
# Few-shot prompt를 생성하기 위해, train data의 일부를 사용합니다.
few_shot_samples = train_df.sample(1) #train_df에서의 임의의 1개 샘플 무작위로 뽑음
#뽑은 row에서 dialogue,summary 컬럼 각각 추출
sample_dialogue1 = few_shot_samples.iloc[0]['dialogue']
sample_summary1 = few_shot_samples.iloc[0]['summary']
# 이 데이터를 프롬프트 앞부분에 few-shot 예시로 사용할 수 있게 됩니다.
print(f"Sample Dialogue1:\n{sample_dialogue1}\n")
print(f"Sample Summary1: {sample_summary1}\n")
# Prompt를 생성하는 함수를 수정합니다.
def build_prompt(dialogue):
system_prompt = "You are a expert in the field of dialogue summarization, summarize the given dialogue in a concise manner. Follow the user's instruction carefully and provide a summary that is relevant to the dialogue."
user_prompt = (
"Following the instructions below, summarize the given document.\n"
"Instructions:\n"
"1. Read the provided sample dialogue and corresponding summary.\n"
"2. Read the dialogue carefully.\n"
"3. Following the sample's style of summary, provide a concise summary of the given dialogue.\n\n"
"Sample Dialogue:\n"
f"{sample_dialogue1}\n\n"
"Sample Summary:\n"
f"{sample_summary1}\n\n"
"Dialogue:\n"
f"{dialogue}\n\n"
"Summary:\n"
)
return [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_prompt
}
]
# few
system_promptuser_promptInstructions:)이 포함되어 있어서 모델이 학습하기에 좋음"Following the sample's style..."를 통해 output style consistency를 유도함# Few-shot sample을 다른 방식으로 사용하여 prompt를 생성합니다.
def build_prompt(dialogue):
system_prompt = "You are a expert in the field of dialogue summarization, summarize the given dialogue in a concise manner. Follow the user's instruction carefully and provide a summary that is relevant to the dialogue."
few_shot_user_prompt_1 = (
"Following the instructions below, summarize the given document.\n"
"Instructions:\n"
"1. Read the provided sample dialogue and corresponding summary.\n"
"2. Read the dialogue carefully.\n"
"3. Following the sample's style of summary, provide a concise summary of the given dialogue. Be sure that the summary is simple but captures the essence of the dialogue.\n\n"
"Dialogue:\n"
f"{sample_dialogue1}\n\n"
"Summary:\n"
)
few_shot_assistant_prompt_1 = sample_summary1
user_prompt = (
"Dialogue:\n"
f"{dialogue}\n\n"
"Summary:\n"
)
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": few_shot_user_prompt_1},
{"role": "assistant", "content": few_shot_assistant_prompt_1},
{"role": "user", "content": user_prompt},
]
| 항목 | 방식 1: 한 덩어리 user prompt | 방식 2: user-assistant role 분리 |
|---|---|---|
| 구조 | system + user(prompt 전체) | system + user + assistant + user |
| 샘플 예시 방식 | 샘플 대화 & 요약을 텍스트로 삽입 | 샘플 대화 → 모델의 응답을 실제 assistant role로 삽입 |
| 모델 관점 | 새로운 대화를 처음 보는 입력처럼 처리 | 이전 대화 흐름을 학습처럼 모방 |
| 대표 활용 | GPT-style instruction tuning | Chat-based fine-tuning (SFT), LangChain 등 |
| 성능 효과 | 명시적 task 지시 효과 有 | 실질적 imitation/few-shot 효과 강함 |
return [
{"role": "system", "content": "..."},
{"role": "user", "content": """
Sample Dialogue: ...
Sample Summary: ...
Dialogue: ...
Summary:
"""}
]
gpt-3.5-turbo 같은 모델에 최적화return [
{"role": "system", "content": "..."},
{"role": "user", "content": "Dialogue: 샘플 대화\nSummary:"},
{"role": "assistant", "content": "샘플 요약"},
{"role": "user", "content": "Dialogue: 실제 대화\nSummary:"},
]
solar, gpt, llama류 모델들이 이 포맷에서 매우 잘 작동함| 상황 | 추천 방식 |
|---|---|
| 명시적으로 "이렇게 요약해"라고 지시하고 싶다 | 방식 1 (한 덩어리 user prompt) |
| Solar / GPT 등 chat 기반 모델을 모방시키고 싶다 | 방식 2 (user + assistant role 분리) ✅ |
| 샘플이 매우 짧다 | 방식 1 |
| 샘플과 입력 길이가 길다 | 방식 1 (token overflow 방지) |
| 모델이 SFT 스타일로 학습된 경우 | 방식 2 ✅ |
user → assistant 쌍을 반복하면 됩니다.