LLM 엔지니어링 · 토크나이저와 임베딩 · 실습
TF-IDF 임베딩과 코사인 검색
목표
TF-IDF 임베딩을 numpy 로 직접 계산하고, 코사인 유사도로 문서를 검색하며, 차원 축소의 대가를 숫자로 확인합니다.
왜 중요한가
임베딩 API 를 호출하면 벡터가 나옵니다. 그 안에서 무슨 일이 일어나는지 모른 채로도 쓸 수는 있지만, 검색 결과가 이상할 때 원인을 좁힐 수 없습니다.
TF-IDF 는 신경망 임베딩보다 훨씬 단순하지만 핵심 구조는 같습니다. 텍스트를 벡터로 바꾸고, 길이를 정규화하고, 내적으로 유사도를 잰다는 뼈대가 동일합니다. 그래서 여기서 정규화를 빼먹으면 긴 문서가 모든 질의에 걸리는 현상을 직접 볼 수 있고, 그 경험이 실제 벡터 검색을 디버깅할 때 그대로 쓰입니다.
마지막 단계의 해싱 트릭도 의미가 있습니다. 차원을 줄이면 메모리와 속도를 얻지만 충돌 때문에 정보를 잃습니다. 그 손실이 실제로 얼마나 되는지 숫자로 재 보면, 벡터 차원을 고를 때의 감각이 생깁니다.
단계
작업 디렉터리는 /root/llm 입니다. 대상은 docs 표의 문서 30건입니다.
1. 각 문서를 토큰화해 /root/llm/doc_tokens.tsv 에 저장합니다. docs.id<탭>공백으로 구분한 토큰들 형식이고 30줄입니다. 토큰화 규칙은 소문자로 바꾼 뒤 정규식 [가-힣a-z0-9]+ 로 뽑아내기 입니다.
2. 항별 문서 빈도를 /root/llm/df.tsv 에 항<탭>df 형식으로 저장합니다. 항은 오름차순 정렬이고, 한 문서에 여러 번 나와도 1로 셉니다.
3. IDF 를 /root/llm/idf.tsv 에 항<탭>idf 형식으로 저장합니다. 항 순서는 2번과 같고 값은 소수점 여섯 자리입니다. 공식은 ln((1 + N) / (1 + df)) + 1 이며 N 은 문서 수입니다.
4. TF-IDF 행렬을 /root/llm/tfidf.npy 로 저장합니다. 모양은 (문서 수, 항 수)이고 항의 순서는 2번과 같습니다. 값은 등장 횟수에 idf 를 곱한 뒤 각 행의 L2 길이를 1로 정규화한 것입니다.
5. 각 문서의 최근접 이웃을 /root/llm/sim_top.tsv 에 docs.id<탭>가장 비슷한 docs.id<탭>점수 로 저장합니다. 자기 자신은 제외하고, 점수는 소수점 여섯 자리이며, 동점이면 docs.id 가 작은 쪽을 고릅니다.
6. 아래 질의 3개에 대해 상위 3개 문서를 /root/llm/query_top3.tsv 에 질의번호<탭>순위<탭>docs.id<탭>점수 로 저장합니다. 총 9줄입니다.
- 1번:
인덱스가 왜 안 타는지 알고 싶다 - 2번:
TCP 연결이 안 될 때 무엇을 보나 - 3번:
GPU 는 왜 메모리 때문에 느려지나
질의 벡터도 같은 토큰화와 같은 idf 를 쓰고 L2 정규화합니다. 동점이면 docs.id 가 작은 쪽이 앞섭니다.
7. 해싱 트릭으로 1024차원 행렬을 만들어 /root/llm/hashed.npy 로 저장합니다. 버킷은 int(md5(항).hexdigest(), 16) % 1024 이고, 각 버킷에 등장 횟수 곱하기 idf 를 누적한 뒤 행을 L2 정규화합니다.
8. 두 방식의 최근접 이웃이 일치하는 문서 수와 비율을 /root/llm/compare.tsv 에 한 줄로 일치수<탭>비율 형식으로 저장합니다. 비율은 소수점 세 자리입니다.
참고
numpy는 설치되어 있습니다.np.save,np.load,np.linalg.norm을 씁니다.- 문서와 항의 순서를 한 번 정하면 끝까지 같은 순서를 유지해야 합니다.
- 흔한 실수 1: 파이썬 내장
hash()는 실행마다 값이 달라집니다. 반드시hashlib.md5를 쓰세요. - 흔한 실수 2: 정규화를 빼먹으면 긴 문서가 모든 질의에 걸립니다. 각 행의 길이가 1인지 확인하세요.
단계 8개
- 문서 토큰화하기
- 문서 빈도 세기
- IDF 계산하기
- TF-IDF 행렬 만들기
- 최근접 이웃 찾기
- 질의로 문서 검색하기
- 해싱 트릭으로 차원 줄이기
- 두 방식의 결과 비교하기