엔지니어링

벡터 필드를 사용한 텍스트 유사도 검색

Elasticsearch는 초창기에 레시피 검색 엔진이었을 때부터 빠르고 강력한 전체 텍스트 검색을 제공하도록 설계되었습니다. 이러한 특성 덕분에 벡터를 사용한 작업을 계속 진행하는 데 필요한 텍스트 검색을 개선하는 과제는 늘 중요한 원동력이었습니다. Elasticsearch 7.0은 고차원 벡터를 위한 실험 필드 유형을 도입했으며 이제 릴리즈 7.3에서는 이러한 벡터를 사용하여 문서 점수를 매길 수 있습니다.

이 게시물은 텍스트 유사도 검색이라는 특정 기술에 초점을 맞춥니다. 이러한 검색 유형에서는 사용자가 짧은 자유 텍스트 쿼리를 입력하면 쿼리와의 유사도에 따라 문서 순위가 매겨집니다. 텍스트 유사도는 다음과 같은 다양한 사용 사례에서 유용합니다.

  • 질문에 대한 답변: 자주 묻는 질문 모음을 통해 사용자가 입력한 질문과 유사한 질문을 찾습니다.
  • 기사 검색: 연구 기사 모음에서 사용자의 쿼리와 밀접하게 관련된 제목의 기사를 반환합니다.
  • 이미지 검색: 캡션이 달린 이미지 데이터 세트에서 사용자의 설명과 유사한 캡션이 있는 이미지를 찾습니다.

쉽게 설명하자면 쿼리의 단어와 일치하는 단어가 문서에 몇 개가 있는지에 따라 문서의 순위를 매기는 것이 유사도 검색입니다. 그러나 일치하는 단어 수가 아주 적더라도 문서가 쿼리와 유사할 수 있으므로 유사도에 대한 강화된 개념에서는 구문론과 의미론적 내용도 고려합니다.

자연어 처리(NLP) 커뮤니티는 단어와 문장을 숫자 벡터로 인코딩하는 텍스트 임베딩이라는 기법을 개발했습니다. 이러한 벡터 표현은 텍스트의 언어적 내용을 포착하도록 설계되었으며 쿼리와 문서 사이의 유사도를 평가하는 데 사용할 수 있습니다.

이 게시물에서는 텍스트 임베딩과 Elasticsearch의 dense_vector 유형을 사용하여 유사도를 검색하는 방법을 살펴봅니다. 우선 임베딩 기술에 대한 개요를 설명한 다음 Elasticsearch를 사용한 유사도 검색의 간단한 프로토타입을 살펴보겠습니다.

참고: 검색 시 텍스트 임베딩 활용은 복잡하고 진화하는 영역입니다. 이 블로그 게시물이 탐색을 위한 도약의 발판이 되기를 바라지만, 특정 검색 아키텍처나 구현을 위한 권고사항은 아닙니다.

텍스트 임베딩이란?

다양한 유형의 텍스트 임베딩을 알아보고 기존 검색 방식과 비교해보겠습니다.

단어 임베딩

단어 임베딩 모델은 단어를 고밀도 숫자 벡터로 나타냅니다. 이러한 숫자 벡터의 목표는 단어의 의미론적 속성을 포착하는 것으로, 벡터가 비슷한 단어들은 의미론적 의미가 유사합니다. 우수한 임베딩에서는 벡터 공간의 방향이 단어 의미의 다양한 측면과 연결되어 있습니다. 예를 들어 ‘캐나다’의 벡터는 한쪽으로는 ‘프랑스’와 가깝고 다른 한쪽으로는 ‘토론토’와 가깝습니다.

NLP와 검색 커뮤니티는 꽤 오래전부터 단어의 벡터 표현에 관심을 두었는데, 지난 몇 년간 기존 작업에 신경망을 적용할 수 있게 되면서 단어 임베딩이 다시 주목받게 되었습니다. word2vec, GloVe를 비롯하여 일부 단어 임베딩 알고리즘이 성공적으로 개발되었습니다. 이러한 접근 방식에서는 대규모 텍스트 모음을 사용하고 각 단어가 나타나는 문맥을 검토하여 벡터 표현을 결정합니다.

  • word2vec Skip-gram 모델은 신경망을 훈련하여 한 문장에서 한 단어 주변의 문맥 단어들을 예측합니다. 각 단어에 대한 신경망의 내부 가중치가 단어 임베딩을 결정합니다.
  • GloVe 모델에서는 단어가 다른 문맥 단어와 함께 나타나는 빈도에 따라 단어의 유사도가 달라집니다. 이 알고리즘은 단어 동시 발생 수에 대한 간단한 선형 모델을 훈련합니다.

Wikipedia 또는 Common Crawl과 같은 대규모 텍스트 코퍼스를 사용해 사전에 훈련된 모델을 배포하는 연구 그룹도 많아 간편하게 다운로드하여 다운스트림 작업에 연결할 수 있습니다. 사전에 훈련된 버전은 바로 사용하는 경우가 많지만, 특정 대상 데이터 세트와 작업에 맞게 모델을 조정하는 것이 유용할 수 있습니다. 대개 사전에 훈련된 모델에서 간단한 미세 조정 단계를 수행하면 됩니다.

단어 임베딩은 상당히 강력하고 효과적인 것으로 입증되었고, 이제 기계 번역이나 감정 분류와 같은 NLP 작업에서 개별 토큰 대신 임베딩을 사용하는 것이 일반적입니다.

문장 임베딩

최근에는 연구원들이 텍스트에서 단어뿐만 아니라 더 긴 구절을 나타내는 임베딩 기법에 집중하기 시작했습니다. 최신 접근 방식 대부분은 복잡한 신경망 아키텍처를 기반으로 하며, 의미론적 정보를 포착하는 데 도움이 되도록 훈련 중에 레이블이 지정된 데이터를 통합하기도 합니다.

일단 훈련된 모델은 문장을 가져다가 문맥에 따라 각 단어에 대한 벡터를 만들 수 있을 뿐만 아니라 전체 문장에 대한 벡터도 만들 수 있습니다. 단어 임베딩과 마찬가지로 많은 모델이 사전에 훈련된 버전을 제공하므로 사용자는 비용이 많이 드는 훈련 프로세스를 건너뛸 수 있습니다. 훈련 프로세스가 매우 리소스 집약적인 반면, 모델 호출은 훨씬 더 간단합니다. 문장 임베딩 모델은 일반적으로 실시간 애플리케이션에 사용할 수 있을 만큼 빠릅니다.

일반적인 문장 임베딩 기법으로는 InferSent, Universal Sentence Encoder, ELMoBERT가 있습니다. 단어 임베딩과 문장 임베딩 개선은 활발하게 진행되는 연구 분야이며 강력한 모델이 추가로 도입될 것으로 보입니다.

기존 검색 방식과 비교

기존 정보 검색에서는 텍스트를 숫자 벡터로 나타낼 때 어휘의 각 단어에 하나의 차원을 지정하는 것이 일반적입니다. 그리고 텍스트 한 부분의 벡터는 어휘의 각 용어가 나타나는 횟수를 기반으로 합니다. 이러한 텍스트 표현 방식은 흔히 “BOW(Bag of Words)”라고 불리는데, 이는 문장 구조와 관계없이 단어 빈도수만 계산하기 때문입니다.

텍스트 임베딩은 다음과 같은 몇 가지 중요한 측면에서 기존 벡터 표현과 다릅니다.

  • 인코딩된 벡터는 밀도가 높고 차원은 상대적으로 낮습니다(주로 100개에서 1,000개 차원). 반면, BOW 벡터는 밀도가 낮고 50,000개 이상의 차원으로 구성될 수 있습니다. 임베딩 알고리즘은 의미론적 의미를 모델링할 때 텍스트를 저차원 공간에 인코딩합니다. 이상적으로는 동의어인 단어와 구문은 새로운 벡터 공간에서 유사한 표현이 됩니다.
  • 문장 임베딩에서는 벡터 표현을 결정할 때 단어 순서를 고려할 수 있습니다. 예를 들어 “tuin in”이라는 구문은 “in tune”과는 매우 다른 벡터로 매핑될 수 있습니다.
  • 실제로 문장 임베딩이 텍스트의 긴 구절을 일반화하는 경우는 드뭅니다. 일반적으로 짧은 단락 수준을 넘는 길이의 텍스트를 나타내는 데는 사용되지 않습니다.

유사도 검색에 임베딩 사용

방대한 양의 질문과 답변 모음이 있다고 가정해 보겠습니다. 사용자가 질문하면, 우리는 사용자가 답을 찾을 수 있도록 이 모음에서 가장 유사한 질문을 검색하고자 합니다.

텍스트 임베딩을 사용하여 유사한 질문을 검색할 수 있습니다.

  • 색인하는 동안 각 질문은 문장 임베딩 모델을 통해 실행되어 숫자 벡터를 생성합니다.
  • 사용자가 쿼리를 입력하면 동일한 문장 임베딩 모델을 통해 실행되어 벡터를 생성합니다. 응답의 순위를 매기기 위해 각 질문과 쿼리 벡터 사이의 벡터 유사도를 계산하게 됩니다. 임베딩 벡터를 비교할 때는 코사인 유사도를 사용하는 것이 일반적입니다.

이 리포지토리는 Elasticsearch에서 이를 구현하는 방법을 간단한 예시를 통해 보여줍니다. 기본 스크립트는 StackOverflow 데이터 세트의 질문을 최대 20,000개까지 색인한 다음 사용자가 데이터 세트에 대한 자유 텍스트 쿼리를 입력할 수 있게 합니다.

이 스크립트의 각 부분을 자세히 짚어보기 전에 몇 가지 예시 결과를 살펴보겠습니다. 대부분의 경우 이 메서드를 사용하면 쿼리와 색인된 질문 사이에 많은 단어가 겹치지 않더라도 유사도를 포착할 수 있습니다.

  • "zipping up files"는 "Compressing / Decompressing Folders & Files"를 반환
  • "determine if something is an IP"는 "How do you tell whether a string is an IP or a hostname"을 반환
  • "translate bytes to doubles"는 "Convert Bytes to Floating Point Numbers in Python"을 반환

구현 세부 정보

이 스크립트는 TensorFlow에 임베딩 모델을 다운로드하고 생성하는 것으로 시작합니다. 여기에서는 Google의 Universal Sentence Encoder를 선택했지만 얼마든지 다른 임베딩 메서드를 사용할 수 있습니다. 이 스크립트에서는 추가 훈련이나 미세 조정 없이 임베딩 모델을 그대로 사용합니다.

그런 다음 질문 제목, 태그, 벡터로 인코딩된 질문 제목에 대한 매핑을 포함하는 Elasticsearch 인덱스를 생성합니다.

"mappings": {
  "properties": {
    "title": {
      "type": "text"
    },
    "title_vector": {
      "type": "dense_vector",
      "dims": 512
    }
    "tags": {
      "type": "keyword"
    },
    ...
  }
}

dense_vector에 대한 매핑에서는 벡터에 포함될 차원 수를 지정해야 합니다. title_vector 필드를 색인할 때 Elasticsearch에서는 차원 수가 매핑에 지정된 것과 동일한지 확인합니다.

문서를 색인하기 위해 임베딩 모델을 통해 질문 제목을 실행하여 숫자 배열을 가져옵니다. 이 배열은 문서에서 title_vector 필드에 추가됩니다.

사용자가 쿼리를 입력하면 텍스트는 먼저 동일한 임베딩 모델을 통해 실행되고 query_vector 파라미터에 저장됩니다. Elasticsearch에서는 7.3부터 cosineSimilarity 함수를 기본 스크립팅 언어로 제공합니다. 따라서 다음과 같이 script_score 쿼리를 사용하여 사용자 쿼리와의 유사도를 기준으로 질문 순위를 매깁니다.

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, doc['title_vector']) + 1.0",
      "params": {"query_vector": query_vector}
    }
  }
}

그리고 모든 새 쿼리에서 스크립트를 다시 컴파일하지 않도록 쿼리 벡터를 스크립트 파라미터 형태로 전달합니다. Elasticsearch에서는 음수 점수를 허용하지 않으므로 코사인 유사도에 음수를 추가해야 합니다.

참고: 이 블로그 게시물은 원래 Elasticsearch 7.3에서 제공되었지만 7.6에서는 더 이상 사용되지 않는 벡터 함수에 대한 다른 구문을 사용했습니다.

주의해야 할 제한 사항

script_score 쿼리는 제한적인 쿼리를 래핑하고 반환하는 문서 점수를 수정하도록 설계되었습니다. 그러나 Elasticsearch에서는 match_all 쿼리를 제공합니다. 즉, 인덱스의 모든 문서에서 스크립트가 실행됩니다. 이는 현재 Elasticsearch의 벡터 유사도에 대한 제한 사항입니다. 즉, 벡터는 문서 점수를 매기는 데 사용할 수 있지만 초기 검색 단계에서는 사용할 수 없습니다. 벡터 유사도에 기초한 검색 지원은 진행 중인 작업의 중요한 영역입니다.

모든 문서를 스캔하지 않고 빠른 성능을 유지하기 위해 match_all 쿼리를 더 선택적인 쿼리로 대체할 수 있습니다. 검색에 적합한 쿼리는 사용 사례별로 달라질 수 있습니다.

위에서 몇 가지 고무적인 사례를 보았지만, 결과가 명확하거나 직관적이지 않을 수도 있다는 점을 유념해야 합니다. 예를 들어 "zipping up files"는 "Partial .csproj Files" 및 "How to avoid .pyc files?"에도 높은 점수를 할당합니다. 이 메서드가 예상치 못한 결과를 반환할 때 문제를 디버깅하는 방법이 항상 명확하지는 않습니다. 다시 말해 각 벡터 구성 요소가 무엇을 의미하는지 불투명하고 그 개념을 해석하기 힘들 때가 많습니다. 단어 중첩에 기반한 전통적인 점수 기법을 사용하면 “이 문서의 순위가 높은 이유는 무엇입니까?”라는 질문에 더 쉽게 답할 수 있습니다.

앞에서 언급한 바와 같이, 이 프로토타입은 임베딩 모델을 생산 가능한 솔루션으로서가 아니라 어떻게 벡터 필드와 함께 사용할 수 있는지를 보여주는 예로서 의도된 것입니다. 새로운 검색 전략을 개발할 때는 자신의 데이터에서 접근 방식이 어떻게 수행되는지 테스트하는 것이 중요하며,match 쿼리 같은 강력한 기준선과 비교해야 합니다. 대상 데이터 세트에 대한 임베딩 모델을 미세 조정하거나, 단어 수준 쿼리 확장 등 임베딩을 통합하는 다른 방식을 시도하는 등 확실한 결과를 얻기 전에 전략을 크게 변경할 필요가 있을 수 있습니다.

결론

임베딩 기법은 텍스트의 언어적 내용을 포착하는 강력한 방법을 제공합니다. 임베딩을 색인하고 벡터 거리를 기반으로 점수를 매기면 단어 수준의 중첩을 넘어 유사도라는 개념으로 문서 순위를 매길 수 있습니다.

벡터 필드 유형을 기반으로 한 더 많은 기능을 소개할 수 있을 것으로 기대합니다. 검색에 벡터를 사용하는 것은 중요하고 미묘한 영역입니다. 늘 그렇듯이 여러분의 사용 사례와 경험에 대해 자세히 듣고 싶습니다. Github토론 포럼에 글을 남겨주세요!