🤖 AI/ML

휴리스틱

Heuristic

문제 해결을 위한 경험적 규칙. 최적해를 보장하지 않지만 빠른 근사치 제공.

📖 상세 설명

휴리스틱(Heuristic)은 복잡한 문제를 빠르게 해결하기 위한 경험 기반의 규칙이나 전략입니다. 최적해를 보장하지는 않지만, 합리적인 시간 내에 충분히 좋은 해를 찾을 수 있습니다. 그리스어 "heuriskein"(발견하다)에서 유래했으며, NP-Hard 문제처럼 완벽한 알고리즘이 실용적이지 않을 때 널리 사용됩니다. AI, 최적화, 검색 알고리즘, 의사결정 시스템에서 핵심 역할을 합니다.

휴리스틱의 역사는 컴퓨터 과학 초기로 거슬러 올라갑니다. 1950년대 허버트 사이먼과 앨런 뉴웰은 인간의 문제 해결이 휴리스틱에 기반한다고 주장했습니다. 체스 프로그램에서 모든 수를 탐색하는 대신 "중앙 통제가 유리하다"는 휴리스틱을 사용한 것이 대표적입니다. 오늘날 A* 알고리즘의 h(n) 함수, 메타휴리스틱(유전 알고리즘, 시뮬레이티드 어닐링), 탐욕 알고리즘 등이 널리 사용됩니다.

AI와 ML에서 휴리스틱은 여전히 중요한 역할을 합니다. 검색 알고리즘에서 탐색 공간을 줄이는 가지치기(pruning), 강화학습의 보상 설계, 하이퍼파라미터 초기값 설정, LLM의 디코딩 전략(beam search의 빔 크기) 등이 휴리스틱입니다. 특히 AutoML에서 탐색 공간을 효율적으로 줄이거나, 신경망 아키텍처 검색(NAS)에서 유망한 구조를 우선 탐색하는 데 휴리스틱이 활용됩니다.

실무에서 휴리스틱은 "빠른 80% 해법"으로 자주 사용됩니다. 복잡한 스케줄링 문제에 가장 긴 작업 먼저(LJF), 추천 시스템에서 최근성(recency) 가중치, 이상 탐지에서 3-시그마 규칙 등이 예입니다. 다만 휴리스틱은 특정 상황에서만 잘 작동할 수 있으므로, 왜 이 규칙이 효과적인지 이해하고 경계 조건을 파악하는 것이 중요합니다. 데이터가 충분하면 ML 모델이 휴리스틱을 대체하거나 보완할 수 있습니다.

💻 코드 예제

다양한 휴리스틱 알고리즘 구현 예제입니다.

import heapq
from typing import Callable, Optional
import random
import math

# 1. A* 알고리즘 - 휴리스틱 함수를 사용한 최단 경로
def a_star_search(
    start: tuple,
    goal: tuple,
    heuristic: Callable[[tuple, tuple], float],
    get_neighbors: Callable[[tuple], list],
    cost: Callable[[tuple, tuple], float] = lambda a, b: 1
):
    """A* 탐색 알고리즘 - h(n) 휴리스틱으로 탐색 효율화"""
    open_set = [(0, start)]  # (f_score, node)
    came_from = {}
    g_score = {start: 0}
    f_score = {start: heuristic(start, goal)}

    while open_set:
        _, current = heapq.heappop(open_set)

        if current == goal:
            # 경로 복원
            path = [current]
            while current in came_from:
                current = came_from[current]
                path.append(current)
            return path[::-1]

        for neighbor in get_neighbors(current):
            tentative_g = g_score[current] + cost(current, neighbor)

            if neighbor not in g_score or tentative_g < g_score[neighbor]:
                came_from[neighbor] = current
                g_score[neighbor] = tentative_g
                f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
                heapq.heappush(open_set, (f_score[neighbor], neighbor))

    return None  # 경로 없음

# 일반적인 휴리스틱 함수들
def manhattan_distance(a: tuple, b: tuple) -> float:
    """맨해튼 거리 휴리스틱 (그리드용)"""
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def euclidean_distance(a: tuple, b: tuple) -> float:
    """유클리드 거리 휴리스틱"""
    return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2)

# 2. 탐욕 알고리즘 - 작업 스케줄링 휴리스틱
def greedy_job_scheduling(jobs: list[tuple[int, int, int]]) -> list[int]:
    """
    작업 스케줄링: (시작시간, 종료시간, 가치)
    휴리스틱: 가장 빨리 끝나는 작업 먼저 (EDF)
    """
    # 종료 시간 기준 정렬 (EDF 휴리스틱)
    sorted_jobs = sorted(enumerate(jobs), key=lambda x: x[1][1])

    selected = []
    last_end = 0

    for idx, (start, end, value) in sorted_jobs:
        if start >= last_end:
            selected.append(idx)
            last_end = end

    return selected

# 3. 시뮬레이티드 어닐링 - 메타휴리스틱
def simulated_annealing(
    initial_state,
    objective: Callable,
    neighbor: Callable,
    initial_temp: float = 1000,
    cooling_rate: float = 0.995,
    min_temp: float = 1
):
    """
    시뮬레이티드 어닐링 - 지역 최적해 탈출 휴리스틱
    """
    current = initial_state
    current_score = objective(current)
    best = current
    best_score = current_score
    temperature = initial_temp

    while temperature > min_temp:
        # 이웃 해 생성
        candidate = neighbor(current)
        candidate_score = objective(candidate)

        # 수락 확률 계산 (휴리스틱)
        delta = candidate_score - current_score

        if delta < 0 or random.random() < math.exp(-delta / temperature):
            current = candidate
            current_score = candidate_score

            if current_score < best_score:
                best = current
                best_score = current_score

        temperature *= cooling_rate

    return best, best_score

# 4. 빔 서치 - LLM 디코딩 휴리스틱
def beam_search_decode(
    model_predict: Callable,  # 다음 토큰 확률 예측 함수
    start_token: int,
    end_token: int,
    beam_width: int = 5,  # 휴리스틱: 탐색 너비
    max_length: int = 50
) -> list[int]:
    """빔 서치 - 확률 기반 탐색 휴리스틱"""
    # (누적 log 확률, 시퀀스)
    beams = [(0.0, [start_token])]

    for _ in range(max_length):
        candidates = []

        for score, sequence in beams:
            if sequence[-1] == end_token:
                candidates.append((score, sequence))
                continue

            # 다음 토큰 확률 예측 (실제로는 모델 호출)
            next_probs = model_predict(sequence)

            # 상위 k개만 고려 (휴리스틱)
            top_k = sorted(enumerate(next_probs), key=lambda x: -x[1])[:beam_width]

            for token, prob in top_k:
                new_score = score + math.log(prob + 1e-10)
                candidates.append((new_score, sequence + [token]))

        # 상위 빔만 유지 (휴리스틱)
        beams = sorted(candidates, key=lambda x: -x[0])[:beam_width]

        if all(seq[-1] == end_token for _, seq in beams):
            break

    return beams[0][1]  # 최고 점수 시퀀스

# 5. 규칙 기반 휴리스틱 - 이상 탐지
class AnomalyDetectionHeuristics:
    """통계 기반 이상 탐지 휴리스틱"""

    @staticmethod
    def three_sigma_rule(data: list[float], value: float) -> bool:
        """3-시그마 규칙: 평균에서 3 표준편차 이상이면 이상"""
        mean = sum(data) / len(data)
        std = (sum((x - mean)**2 for x in data) / len(data)) ** 0.5
        return abs(value - mean) > 3 * std

    @staticmethod
    def iqr_rule(data: list[float], value: float) -> bool:
        """IQR 규칙: Q1 - 1.5*IQR 미만 또는 Q3 + 1.5*IQR 초과면 이상"""
        sorted_data = sorted(data)
        n = len(sorted_data)
        q1 = sorted_data[n // 4]
        q3 = sorted_data[3 * n // 4]
        iqr = q3 - q1
        return value < q1 - 1.5 * iqr or value > q3 + 1.5 * iqr

    @staticmethod
    def sudden_change(current: float, previous: float, threshold: float = 0.5) -> bool:
        """급격한 변화 감지: 이전 값 대비 threshold% 이상 변화"""
        if previous == 0:
            return abs(current) > threshold
        return abs((current - previous) / previous) > threshold

# 사용 예시
if __name__ == "__main__":
    # A* 경로 탐색
    print("=== A* 경로 탐색 ===")
    def get_grid_neighbors(pos):
        x, y = pos
        return [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]

    path = a_star_search(
        start=(0, 0),
        goal=(5, 5),
        heuristic=manhattan_distance,
        get_neighbors=get_grid_neighbors
    )
    print(f"최단 경로: {path}")

    # 작업 스케줄링
    print("\n=== 탐욕 작업 스케줄링 ===")
    jobs = [(0, 3, 10), (2, 5, 15), (4, 7, 12), (6, 9, 20)]
    selected = greedy_job_scheduling(jobs)
    print(f"선택된 작업 인덱스: {selected}")

    # 이상 탐지
    print("\n=== 이상 탐지 휴리스틱 ===")
    data = [10, 12, 11, 13, 10, 12, 11, 100]  # 100이 이상치
    detector = AnomalyDetectionHeuristics()
    print(f"3-시그마 이상치: {detector.three_sigma_rule(data[:-1], 100)}")
    print(f"IQR 이상치: {detector.iqr_rule(data[:-1], 100)}")

📊 휴리스틱 유형과 활용

유형 설명 예시 활용 분야
탐욕 휴리스틱 각 단계에서 최선 선택 가장 가까운 이웃 먼저 스케줄링, TSP
구성 휴리스틱 해를 점진적으로 구축 A* 알고리즘 경로 탐색, 게임 AI
개선 휴리스틱 기존 해를 반복 개선 2-opt, 3-opt VRP, 조합 최적화
메타휴리스틱 탐색 전략의 프레임워크 유전 알고리즘, SA NP-Hard 문제
규칙 기반 휴리스틱 도메인 전문가 경험 3-시그마 규칙 이상 탐지, 품질 관리

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"배송 경로 최적화에 완벽한 알고리즘은 시간이 너무 오래 걸려요. 가장 가까운 곳 먼저 방문하는 탐욕 휴리스틱으로 시작하고, 시간이 남으면 2-opt로 개선하는 게 현실적입니다."
💬 면접에서
"휴리스틱은 최적해를 보장하지 않지만 합리적인 시간에 충분히 좋은 해를 찾는 경험적 규칙입니다. A* 알고리즘에서 맨해튼 거리 휴리스틱을 사용하면 탐색 공간을 크게 줄여 효율적으로 최단 경로를 찾을 수 있습니다."
💬 기술 토론에서
"LLM 디코딩에서 빔 서치의 빔 크기가 휴리스틱이에요. 크면 품질이 좋지만 느리고, 작으면 빠르지만 최적을 놓칠 수 있죠. 보통 5-10 정도가 경험적으로 좋은 균형점입니다."

⚠️ 흔한 실수 & 주의사항

휴리스틱을 항상 최적이라고 가정

휴리스틱은 "충분히 좋은" 해를 빨리 찾는 것이지 최적해를 보장하지 않습니다. 비용 민감한 결정에서는 휴리스틱 결과를 기준선으로 하고 추가 최적화를 고려하세요.

도메인 변경 시 검증 없이 휴리스틱 재사용

특정 도메인에서 효과적인 휴리스틱이 다른 도메인에서도 잘 작동하리라는 보장이 없습니다. 새로운 상황에서는 반드시 검증하세요.

휴리스틱과 ML 모델 조합 고려

데이터가 충분하면 휴리스틱을 ML 모델로 대체하거나, 휴리스틱을 ML 모델의 피처나 가이드로 활용하세요. 두 접근의 장점을 결합할 수 있습니다.

🔗 관련 용어

📚 더 배우기