TT Lab
시작하기
배우기 러닝패스 코스

LLM 엔지니어링

TF-IDF 임베딩과 코사인 검색

TT Lab 에서 이어서 보기

목표

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 에 한 줄로 일치수<탭>비율 형식으로 저장합니다. 비율은 소수점 세 자리입니다.

참고

문서 토큰화하기

각 문서를 토큰화해 /root/llm/doc_tokens.tsv 에 저장합니다. docs.id<탭>공백으로 구분한 토큰들 형식이고 30줄입니다. 토큰화 규칙은 소문자로 바꾼 뒤 정규식 [가-힣a-z0-9]+ 로 뽑아내기 입니다.

소문자로 바꾼 뒤 한글, 영문, 숫자가 이어진 덩어리만 남깁니다. 정규식 하나면 됩니다.

문서 빈도 세기

항별 문서 빈도를 /root/llm/df.tsv 에 항<탭>df 형식으로 저장합니다. 항은 오름차순 정렬이고, 한 문서에 여러 번 나와도 1로 셉니다.

한 문서에 여러 번 나와도 1로 셉니다. 항은 오름차순으로 정렬합니다.

IDF 계산하기

IDF 를 /root/llm/idf.tsv 에 항<탭>idf 형식으로 저장합니다. 항 순서는 2번과 같고 값은 소수점 여섯 자리입니다. 공식은 ln((1 + N) / (1 + df)) + 1 이며 N 은 문서 수입니다.

분모와 분자에 1을 더하고 마지막에 1을 더하는 평활 공식을 씁니다.

TF-IDF 행렬 만들기

TF-IDF 행렬을 /root/llm/tfidf.npy 로 저장합니다. 모양은 (문서 수, 항 수)이고 항의 순서는 2번과 같습니다. 값은 등장 횟수에 idf 를 곱한 뒤 각 행의 L2 길이를 1로 정규화한 것입니다.

등장 횟수에 idf 를 곱한 뒤 행 단위로 길이를 1로 맞춥니다. 순서를 바꾸면 값이 달라집니다.

최근접 이웃 찾기

각 문서의 최근접 이웃을 /root/llm/sim_top.tsv 에 docs.id<탭>가장 비슷한 docs.id<탭>점수 로 저장합니다. 자기 자신은 제외하고, 점수는 소수점 여섯 자리이며, 동점이면 docs.id 가 작은 쪽을 고릅니다.

정규화했으므로 내적이 곧 코사인 유사도입니다. 자기 자신은 후보에서 빼세요.

질의로 문서 검색하기

아래 질의 3개에 대해 상위 3개 문서를 /root/llm/query_top3.tsv 에 질의번호<탭>순위<탭>docs.id<탭>점수 로 저장합니다. 총 9줄입니다.

질의도 같은 idf 로 벡터를 만들고 정규화합니다. 학습에 없던 단어는 무시합니다.

해싱 트릭으로 차원 줄이기

해싱 트릭으로 1024차원 행렬을 만들어 /root/llm/hashed.npy 로 저장합니다. 버킷은 int(md5(항).hexdigest(), 16) % 1024 이고, 각 버킷에 등장 횟수 곱하기 idf 를 누적한 뒤 행을 L2 정규화합니다.

파이썬 내장 hash 는 실행마다 값이 달라집니다. md5 를 써야 재현됩니다.

두 방식의 결과 비교하기

두 방식의 최근접 이웃이 일치하는 문서 수와 비율을 /root/llm/compare.tsv 에 한 줄로 일치수<탭>비율 형식으로 저장합니다. 비율은 소수점 세 자리입니다.

각 문서의 최근접 이웃이 두 방식에서 같은지 세어 비율을 냅니다.