글자 대신 뜻을 잰다 — 문장을 숫자로 바꾼다
Article
앞에서 글 264편을 9,005조각으로 잘랐다. 이제 여기서 답을 찾아야 한다.
- ①② 웹에서 글을 가져온다끝남
- ③④ 토막으로 자른다끝남
- ⑤ 문장을 숫자로 바꾼다지금 여기
- ⑥ 264편을 검색한다다음
글자를 맞춰보는 방식이 안 되는 이유
- 주문 후 7일 이내에 환불 신청이 가능하다. 겹치는 글자 1개
- 배송은 결제일 기준 2~3일 걸린다. 겹치는 글자 1개
- 회원 등급은 매월 1일에 갱신된다. 겹치는 글자 1개
- 포인트는 적립일로부터 1년간 유효하다. 겹치는 글자 1개
전부 1개다. 답이 첫 줄에 있는데 넷을 구분할 방법이 없다. 뜻을 봐야 하는데 컴퓨터는 숫자만 안다. 그래서 뜻을 숫자로 바꾼다.
- 벡터
- 줄지어 늘어놓은 숫자 묶음
- [0.08, -0.04, ...]
- 임베딩
- 문장을 벡터로 바꾸는 일 (또는 그 결과)
- 뜻을 좌표로
- 차원
- 그 묶음에 든 숫자의 개수
- 지도는 2, 우리 모델은 384
지도에서 서울 (37.5, 127.0) 과 인천 (37.4, 126.7) 의 숫자를 빼보면 어디가 가까운지 나온다. 원리는 같고, 뜻은 축이 많아서 384개를 쓸 뿐이다.
1. 도구를 깔고 모델을 받는다
.venv\Scripts\activate # (.venv) 가 붙었는지 확인하고
python -m pip install sentence-transformers
계산 엔진(torch)이 딸려 와서 1~2GB 를 받는다. 10분 넘게 걸려도 멈춘 게 아니다. 그래픽카드는 없어도 된다.
from sentence_transformers import SentenceTransformer
# 모델 이름은 "만든사람/모델이름" 형식이다.
# 처음 실행할 때만 받고, 두 번째부터는 디스크에서 읽는다.
model = SentenceTransformer("intfloat/multilingual-e5-small")
print("차원:", len(model.encode("아무 문장이나")))
print("한 번에 읽는 최대 길이:", model.max_seq_length)
- model.safetensors: 100%|██████████| 471M/471M [00:41<00:00, 11.4MB/s]
- 차원: 384
- 한 번에 읽는 최대 길이: 512
이 모델은 문장 하나를 숫자 384개로 표현한다. 512 를 넘으면 뒤가 조용히 잘리는데, 우리 조각은 최대 200자라 한참 남는다.
같이 뜨는 경고 끄기
- Warning: You are sending unauthenticated requests to the HF Hub.
- Please set a HF_TOKEN to enable higher rate limits and faster downloads.
에러가 아니다. 실행할 때마다 모델 저장소에 「최신 맞나」를 확인하러 가는데 그 요청이 익명이라 뜬다. 같은 곳에서 여러 명이 동시에 하면 429 로 차단당하므로, 이미 받았으면 아예 못 가게 막는다.
# 윈도우 파워셸 — 이 터미널에만 적용된다
$env:HF_HUB_OFFLINE = "1"
python 11_load_model.py
# 코드에 넣을 때는 반드시 import 보다 위에
import os
os.environ["HF_HUB_OFFLINE"] = "1"
from sentence_transformers import SentenceTransformer
| 경고 | 모델 로드 | |
|---|---|---|
| 그냥 실행 | 뜬다 | 12.5초 |
HF_HUB_OFFLINE=1 | 없다 | 7.3초 |
2. 문장이 숫자 384개가 된다
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/multilingual-e5-small")
# encode 가 "문장 -> 벡터" 변환을 한다
v = model.encode("환불은 어떻게 하나요")
print(v.shape)
print(v[:6]) # 앞 6개만. 384개를 다 찍으면 화면이 넘친다
# 목록으로 넣으면 여러 문장을 한 번에 처리한다. 반복문보다 빠르다.
vs = model.encode([
"환불은 어떻게 하나요",
"결제를 취소하고 싶어요",
"오늘 점심 뭐 먹지",
])
print(vs.shape)
- (384,)
- [ 0.08457815 -0.04076722 -0.08963361 -0.05305987 0.03292692 0.00353625]
- (3, 384)
.shape 는 「몇 줄 몇 칸인가」다. (384,) 는 묶음 하나, (3, 384) 는 그런 묶음이 3개다.
숫자 하나하나에는 뜻이 없다. 384개가 모여서 만드는 방향이 뜻이다.
3. 글자가 안 겹쳐도 찾는다
두 벡터가 얼마나 비슷한지는 코사인 유사도로 잰다. 두 화살표가 같은 쪽을 가리키는가를 −1 에서 1 사이 숫자로 나타낸 것이다. 1 에 가까우면 같은 방향, 0 근처면 상관없음이다.
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("intfloat/multilingual-e5-small")
question = "돈 돌려받고 싶다"
docs = [
"주문 후 7일 이내에 환불 신청이 가능하다.",
"배송은 결제일 기준 2~3일 걸린다.",
"회원 등급은 매월 1일에 갱신된다.",
"포인트는 적립일로부터 1년간 유효하다.",
]
# 결과가 [[a, b, c, d]] 표 모양이라 [0] 으로 첫 줄만 꺼낸다
scores = util.cos_sim(model.encode(question), model.encode(docs))[0]
# 마이너스를 붙이면 큰 것부터 정렬된다
for i in sorted(range(len(docs)), key=lambda i: -float(scores[i])):
same = set(question.replace(" ", "")) & set(docs[i].replace(" ", ""))
print(f"{float(scores[i]):.3f} 겹치는 글자 {len(same)}개 {docs[i]}")
- 0.844 겹치는 글자 1개 주문 후 7일 이내에 환불 신청이 가능하다.
- 0.831 겹치는 글자 1개 포인트는 적립일로부터 1년간 유효하다.
- 0.827 겹치는 글자 1개 배송은 결제일 기준 2~3일 걸린다.
- 0.799 겹치는 글자 1개 회원 등급은 매월 1일에 갱신된다.
환불 문서가 1위로 올라왔다. 겹치는 글자는 여전히 넷 다 1개다.
4. 점수는 순위로만 쓴다
1위가 0.844, 꼴찌가 0.799 다. 차이가 0.045 밖에 안 난다. 두 문장만 떼어놓고 재면 이렇게 된다.
- 0.888 돈 돌려받고 싶다 / 환불 신청이 가능하다
- 0.892 돈 돌려받고 싶다 / 배송비가 무료다
- 0.863 돈 돌려받고 싶다 / 오늘 점심 뭐 먹지
무관한 쪽이 더 높게 나왔다. 점수대가 통째로 높은 쪽에 뭉쳐 있고, 그 범위는 모델마다 다르다. 「0.8 넘으면 비슷하다」 같은 기준은 없다. 후보끼리 줄 세울 때만 쓴다.
5. 모델 고르기
| 모델 | 차원 | 층 | 최대 토큰 | 받는 용량 |
|---|---|---|---|---|
| multilingual-e5-small | 384 | 12 | 512 | 약 470MB |
| multilingual-e5-base | 768 | 12 | 514 | 약 1.1GB |
| multilingual-e5-large | 1024 | 24 | 514 | 약 2.2GB |
| BAAI/bge-m3 | 1024 | 24 | 8194 | 약 2.2GB |
| ko-sroberta-multitask | 768 | 12 | 514 | 약 440MB |
고르는 순서는 ① 돌릴 수 있나 ② 한국어를 학습했나(multilingual · m3) ③ 검색용(retrieval)으로 학습됐나 ④ 재본다. 우리는 노트북 CPU 로 돌려야 해서 small 을 골랐다.
모델을 바꾸면 벡터를 전부 다시 만들어야 한다. 차원이 같아도 A 모델 벡터와 B 모델 벡터는 비교할 수 없다.
이번 편에 나온 것
| 쓴 것 | 하는 일 |
|---|---|
pip install sentence-transformers | 문장 → 벡터 도구를 깐다 (1~2GB) |
SentenceTransformer("이름") | 모델을 부른다. 없으면 받는다 |
model.encode("문장") | 숫자 384개짜리 벡터 하나 |
model.encode([...]) | 목록을 통째로. 반복문보다 빠르다 |
.shape | 몇 줄 몇 칸인가 |
util.cos_sim(a, b) | 두 벡터의 방향이 얼마나 같나 (−1 ~ 1) |
model.max_seq_length | 이 길이를 넘으면 조용히 잘린다 |
HF_HUB_OFFLINE=1 | 모델 확인 요청을 끈다 |
sorted(..., key=lambda i: -x) | 마이너스를 붙여 큰 것부터 정렬 |
set(a) & set(b) | 두 모음의 공통 부분 |
다음 편에서 9,005조각에 통째로 걸어본다.