이번 편에서 다루는 것
이전 편에서 언급한 문제:
강좌 설명이 부족하면 임베딩 품질이 떨어진다.
실제 데이터를 보니, 상당수의 강좌가 설명이 없거나 너무 짧았다.
이 문제를 LLM(Large Language Model)을 활용해 해결했다.
문제 분석
데이터 현황
강좌 설명을 분석해보니:
| 상태 | 비율 | 예시 |
| 설명 없음 | 15% | NULL 또는 빈 문자열 |
| 이름과 동일 | 20% | "데이터베이스" → "데이터베이스" |
| 너무 짧음 | 25% | "SQL 기초 학습" (10자 미만) |
| 충분함 | 40% | 2줄 이상의 설명 |
60%의 강좌가 불충분한 설명을 가지고 있었다.
임베딩에 미치는 영향
# 설명이 부족한 강좌
course_1 = "데이터베이스"
# 설명이 충분한 강좌
course_2 = "데이터베이스 설계 원리와 SQL 쿼리 작성법을 학습합니다. 정규화, 인덱스, 트랜잭션 처리를 다룹니다."
두 강좌 모두 DB 관련이지만, 임베딩 품질은 크게 다르다.
해결 전략: LLM으로 설명 생성
아이디어
강좌명과 학과 정보를 바탕으로 LLM에게 설명을 생성하도록 요청한다.
입력: 강좌명="데이터베이스", 학과="컴퓨터공학과"
출력: "데이터베이스의 기초 개념과 설계 원리를 학습합니다. 관계형 데이터베이스,
SQL 쿼리 작성, 정규화, 인덱스 등을 다룹니다."
모델 선택
모델 크기 특징 선택 여부
| 모델 | 크기 | 특징 | 선택여부 |
| GPT-4 | - | 최고 품질 | ❌ API 비용, 외부 의존성 |
| Llama 70B | 70B | 우수한 품질 | ❌ GPU 필수 |
| Llama 7B | 7B | 적절한 품질 | ❌ 한국어 성능 부족 |
| 경량 한국어 LLM | 1.2B | 한국어 특화, CPU 실행 | ✅ 선택 |
선택 근거:
- 운영 서버에 GPU가 없음
- 한국어 강좌 설명 생성이 목적
- 배치 작업이므로 실시간 속도는 덜 중요
기본 구현
LLM 로드
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
class DescriptionEnhancer:
def __init__(self, model_path: str):
print(f"LLM 로드 중: {model_path}")
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float32, # CPU에서는 float32
device_map="cpu"
)
print("LLM 로드 완료")
def generate(self, prompt: str, max_new_tokens: int = 150) -> str:
"""프롬프트에 대한 응답 생성"""
inputs = self.tokenizer(prompt, return_tensors="pt")
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=0.7,
top_p=0.9,
pad_token_id=self.tokenizer.eos_token_id
)
generated = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
# 프롬프트 부분 제거하고 생성된 부분만 반환
response = generated[len(prompt):].strip()
return response
프롬프트 설계
프롬프트 설계가 결과 품질에 큰 영향을 미쳤다.
def _build_prompt(self, course_name: str, department: str) -> str:
"""강좌 설명 생성을 위한 프롬프트 구성"""
prompt = f"""### 지시사항
다음 대학교 강좌에 대한 간결한 설명을 2-3문장으로 작성하세요.
학생들이 이 강좌에서 무엇을 배우는지 이해할 수 있도록 작성하세요.
### 강좌 정보
- 강좌명: {course_name}
- 개설 학과: {department}
### 강좌 설명
"""
return prompt
전체 파이프라인
class CourseDescriptionEnhancer:
def __init__(self, model_path: str):
self.enhancer = DescriptionEnhancer(model_path)
def enhance_course(self, course: Dict[str, Any]) -> str:
"""단일 강좌 설명 보강"""
name = course['name']
department = course.get('department', '')
original_desc = course.get('description', '')
# 설명이 충분하면 그대로 반환
if self._is_sufficient(original_desc, name):
return original_desc
# LLM으로 설명 생성
prompt = self._build_prompt(name, department)
generated = self.enhancer.generate(prompt)
# 기존 설명과 결합
if original_desc:
enhanced = f"{original_desc} {generated}"
else:
enhanced = generated
return enhanced
def _is_sufficient(self, description: str, name: str) -> bool:
"""설명이 충분한지 판단"""
if not description:
return False
# 이름과 동일하면 불충분
if description.strip() == name.strip():
return False
# 너무 짧으면 불충분 (30자 미만)
if len(description) < 30:
return False
return True
생성 결과 예시
예시 1: 설명 없는 강좌
입력:
- 강좌명: "데이터베이스"
- 학과: "컴퓨터공학과"
- 기존 설명: (NULL)
생성된 설명:
"데이터베이스의 기초 개념과 설계 원리를 학습합니다. 관계형 데이터베이스 모델링, SQL 쿼리 작성, 정규화 이론을 다루며 실습을 통해 데이터베이스 설계 능력을 기릅니다."
예시 2: 짧은 설명
입력:
- 강좌명: "머신러닝"
- 학과: "인공지능학과"
- 기존 설명: "ML 기초"
보강된 설명:
"ML 기초. 머신러닝의 핵심 알고리즘과 원리를 학습합니다. 지도학습, 비지도학습, 딥러닝 기초를 다루며 Python을 활용한 실습을 진행합니다."
예시 3: 충분한 설명 (보강 안 함)
입력:
- 강좌명: "웹프로그래밍"
- 기존 설명: "HTML, CSS, JavaScript를 활용한 웹 페이지 제작을 학습합니다. 반응형 웹 디자인과 기초적인 프론트엔드 개발을 다룹니다."
결과: 보강 없이 기존 설명 그대로 사용
배치 처리
2,000개 강좌 중 약 1,200개가 보강 대상이었다.
class BatchEnhancer:
def __init__(self, enhancer: CourseDescriptionEnhancer):
self.enhancer = enhancer
def enhance_all(self, courses: List[Dict]) -> Dict[str, str]:
"""전체 강좌 설명 보강"""
results = {}
total = len(courses)
enhanced_count = 0
for i, course in enumerate(courses):
course_id = course['id']
original = course.get('description', '')
# 보강 실행
enhanced = self.enhancer.enhance_course(course)
results[course_id] = enhanced
# 보강 여부 확인
if enhanced != original:
enhanced_count += 1
# 진행 상황 출력
if (i + 1) % 100 == 0:
print(f"진행: {i + 1}/{total} (보강: {enhanced_count}건)")
print(f"완료: 총 {enhanced_count}건 보강")
return results
초기 성능 문제
문제 발견
처음 실행했을 때 성능이 매우 느렸다.
1개 강좌 보강: 약 32초
1,200개 보강 예상 시간: 약 10시간 40분
이건 너무 느리다. 원인을 분석했다.
원인 분석
- 데이터 타입 문제: 모델을 float16으로 로드했음
- CPU 특성: CPU는 float16 연산에 최적화되어 있지 않음
- 변환 오버헤드: 매 연산마다 float16 ↔ float32 변환 발생
이 문제는 다음 편에서 자세히 다룬다. (27배 성능 향상)
품질 검증
생성된 설명이 실제로 임베딩 품질을 개선하는지 확인했다.
검증 방법
def evaluate_embedding_quality(
original_desc: str,
enhanced_desc: str,
related_terms: List[str],
engine: EmbeddingEngine
) -> Dict[str, float]:
"""임베딩 품질 비교"""
# 각각 임베딩
original_emb = engine.encode(original_desc) if original_desc else None
enhanced_emb = engine.encode(enhanced_desc)
# 관련 용어들과의 유사도 계산
term_embs = engine.encode(related_terms)
results = {}
for i, term in enumerate(related_terms):
term_emb = term_embs[i]
if original_emb is not None:
original_sim = engine.similarity(original_emb, term_emb)
results[f"{term}_original"] = original_sim
enhanced_sim = engine.similarity(enhanced_emb, term_emb)
results[f"{term}_enhanced"] = enhanced_sim
return results
검증 결과 예시
강좌: "데이터베이스"
관련 용어 원본 유사도 보강 후 유사도 개선
| 관련 용어 원본 | 유사도 | 보강 후 유사도 | 개선 |
| SQL | 0.42 | 0.78 | +0.36 |
| 데이터 설계 | 0.51 | 0.82 | +0.31 |
| 정규화 | 0.38 | 0.71 | +0.33 |
| 테이블 | 0.45 | 0.69 | +0.24 |
평균 유사도 개선: +0.31
임베딩 품질이 명확히 개선되었다.
DB 저장 및 업데이트
보강된 설명을 DB에 저장한다.
def update_course_descriptions(
self,
enhanced_descriptions: Dict[str, str]
):
"""보강된 설명을 DB에 업데이트"""
for course_id, description in enhanced_descriptions.items():
stmt = (
update(CourseTable)
.where(CourseTable.id == course_id)
.values(description_enhanced=description)
)
self.session.execute(stmt)
self.session.commit()
print(f"{len(enhanced_descriptions)}건 업데이트 완료")
설계 포인트: 원본 설명은 보존하고, 보강된 설명은 별도 컬럼에 저장
CLI 명령어
@app.command()
def enhance(
year: int = typer.Option(None, "--year", "-y", help="대상 연도"),
no_save: bool = typer.Option(False, "--no-save", help="DB 저장 안 함"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="상세 로그")
):
"""강좌 설명을 LLM으로 보강"""
enhancer = CourseDescriptionEnhancer(model_path)
courses = load_courses(year=year)
print(f"대상 강좌: {len(courses)}건")
results = enhancer.enhance_all(courses)
if not no_save:
update_course_descriptions(results)
print("DB 저장 완료")
else:
print("DB 저장 건너뜀")
실행:
# 전체 보강
python main.py enhance
# 특정 연도만
python main.py enhance --year 2024
# 테스트 (저장 안 함)
python main.py enhance --no-save
성과 및 교훈
성과
| 지표 | 수치 |
| 보강 대상 | 1,200개 (60%) |
| 보강 성공 | 1,180개 (98%) |
| 임베딩 품질 개선 | 평균 +0.31 |
배운 점
- 데이터 품질이 우선: 알고리즘보다 데이터 품질이 중요
- LLM은 도구: 적재적소에 활용하면 효과적
- 프롬프트 설계: 명확한 지시가 좋은 결과를 낳는다
- 품질 검증 필수: 생성 결과를 반드시 검증
다음 편 예고
이번 편에서 언급한 성능 문제를 다음 편에서 해결한다.
- CPU에서 float16 vs float32
- 27배 성능 향상의 비밀
- 병목 분석 방법
시리즈 목차
- 프로젝트 소개 - 왜 AI 추천이 필요했나
- 추천을 위한 데이터 파이프라인 설계
- XLM-R로 강좌 임베딩 구축하기
- 추천 알고리즘 설계 - 가중치 기반 개인화
- LLM으로 강좌 설명 보강하기 ← 현재 글
- 성능 최적화 - CPU에서 27배 빠르게
- API 서버 구축과 배포
'프로그래밍 > AI 교과 추천 시스템 개발기' 카테고리의 다른 글
| 6편. 성능 최적화 - CPU에서 27배 빠르게 (0) | 2026.02.20 |
|---|---|
| 4편. 추천 알고리즘 설계 - 가중치 기반 개인화 (0) | 2026.02.18 |
| 3편. XLM-R로 강좌 임베딩 구축하기 (0) | 2026.02.17 |
| 2편. 추천을 위한 데이터 파이프라인 설계 (0) | 2026.02.16 |
| 1편. 프로젝트 소개 - 왜 AI 추천이 필요했나 (0) | 2026.02.15 |