13. WEAT, 임베딩의 연관을 숫자로 읽기
복습 음성 · 21분 43초 · 12·13편 임베딩·WEAT 통합 복습
임베딩의 유사한 단어 목록에는 말뭉치에서 배운 관계가 드러난다. 수집한 문서의 특징이나 반복된 고정관념도 섞일 수 있다. WEAT는 두 대상 집합이 두 속성 집합과 얼마나 다르게 연결되는지 수치로 살펴보는 방법이다.
위 복습 음성은 12편과 같은 「워드 임베딩과 WEAT, 벡터화부터 연관 측정까지」다. 앞부분에는 임베딩 기초가 포함돼 있다. 본문 계산에는 원리를 확인하려고 직접 정한 작은 가상 벡터를 사용했다.
1. 임베딩의 연관은 어디에서 오나
Word2Vec이나 GloVe는 말뭉치의 사용 패턴으로 단어 표현을 배운다. 어떤 직업 단어가 특정 표현과 자주 비슷한 문맥에 나오면, 그 관계가 벡터 공간에도 나타날 수 있다.
이때 세 가지를 구분해서 읽어야 한다. 원문에 함께 나온 데이터의 연관, 학습한 공간에서 가까운 벡터의 연관, 모델을 실제 서비스에 썼을 때 생기는 판단과 영향이다. 서로 연결될 수 있지만 같은 측정값은 아니다.
벡터의 연관을 수치로 확인하면 눈으로 이웃 단어 몇 개만 보는 것보다 비교 기준을 분명하게 잡을 수 있다. 어떤 단어 목록과 모델을 썼는지 남겨야 다른 사람도 그 수치가 뜻하는 범위를 알 수 있다.
2. WEAT는 네 집합을 정하고 시작한다
WEAT, Word Embedding Association Test(단어 임베딩 연관 검사)는 두 Target Set(대상 집합)과 두 Attribute Set(속성 집합)을 비교한다.
- X: 비교할 첫 번째 대상 집합
- Y: 비교할 두 번째 대상 집합
- A: 연관을 재는 첫 번째 속성 집합
- B: 연관을 재는 두 번째 속성 집합
예를 들어 X를 꽃 단어, Y를 곤충 단어로 두고 A를 유쾌함, B를 불쾌함 관련 단어로 둘 수 있다. 질문은 ‘X와 Y가 A·B에 보이는 상대적인 가까움이 다른가’다. 단어 하나씩만 골라 비교하면 특정 단어의 여러 뜻에 크게 흔들릴 수 있어 집합으로 묶는다.
WEAT는 심리학의 IAT, Implicit Association Test(암묵적 연관 검사)에서 비교 구도를 가져왔다. 사람의 반응 시간을 재는 IAT와 달리 WEAT는 단어 벡터 사이의 코사인 유사도를 계산한다. 사람이 가진 태도와 같은 숫자로 직접 비교하는 검사는 아니다. 정의와 연구 배경은 Caliskan 등의 논문을 참고했다.
3. 단어 하나의 점수부터 계산한다
대상 단어 w가 A에 얼마나 가까운지 평균을 내고, B에 얼마나 가까운지 평균을 내서 뺀다.
s(w, A, B)
= A의 단어들과 구한 cosine의 평균
- B의 단어들과 구한 cosine의 평균
가령 A와의 평균 유사도가 0.6이고 B와는 0.2라면 점수는 0.4다. A 쪽에 상대적으로 더 가깝다. 두 값이 0.1과 0.5라면 점수는 -0.4다.
점수가 0이라는 사실만으로 두 속성과 아무 관계가 없다고 할 수는 없다. 둘 다 0.8이거나 둘 다 0.1이어도 차이는 0이다. 이 식은 두 속성 사이의 차이를 보고 있다.
이 점수는 확률이 아니다. 코사인이 -1에서 1 사이이므로 두 평균의 차이는 -2에서 2 사이가 될 수 있다. 뒤의 실행 결과에서 1보다 큰 단어 점수가 나오는 이유다.
4. 검정 통계량과 효과 크기
단어별 s를 구했다면 X와 Y의 차이를 모을 수 있다. 원 논문의 Test Statistic(검정 통계량)은 합의 차이다.
S(X, Y, A, B) = X의 s 점수 합 - Y의 s 점수 합
Effect Size(효과 크기) d는 평균의 차이를 표준편차로 나눈다.
d = (X의 s 평균 - Y의 s 평균)
/ (X와 Y를 합친 s 점수의 표준편차)
분자는 두 대상 집합의 평균 차이이고 분모는 전체 대상 단어 점수의 퍼짐이다. d는 그 퍼짐에 비해 평균 차이가 어느 정도인지 나타낸다. 여기서는 NumPy의 std(ddof=0)을 사용한다. 표본 표준편차인 ddof=1을 쓰면 수치가 달라지므로 비교할 때 설정을 함께 기록한다.
X와 Y의 개수가 같으면 합의 차이와 평균의 차이는 일정한 배수 관계다. 이번 예제는 두 집합의 크기를 각각 3으로 맞췄다. 서로 다른 크기의 집합을 그대로 가져왔다면 같은 검정 설정인지부터 확인해야 한다.
모든 s 점수가 같아 표준편차가 0이면 위 식의 d를 계산할 수 없다. 이 경우를 편향 없음이라는 의미의 0점으로 바꿔 쓰면 계산 불가 상태가 가려진다.
5. 부호는 집합의 순서와 함께 읽는다
양의 d는 X가 Y보다 A 쪽에 상대적으로 더 연결되는 방향이다. 흔히 X↔A, Y↔B 방향이라고 줄여 말한다. X가 A에 절대적으로 가깝고 Y가 B에 절대적으로 가깝다는 두 사실을 각각 증명한 것은 아니다.
X와 Y의 순서를 바꾸면 분자의 부호가 뒤집힌다. A와 B만 바꾸어도 모든 s의 부호가 뒤집힌다. 두 쌍을 모두 바꾸면 다시 원래 부호가 된다.
그래서 ‘WEAT가 -1.2였다’만 남기면 해석이 어렵다. 어떤 단어들이 X·Y·A·B였는지, 양수가 어느 방향인지 함께 적어야 한다. 같은 관계를 반대로 배치하면 숫자 부호도 바뀐다.
6. p값은 다른 질문에 답한다
d가 평균 차이의 크기를 나타낸다면, Permutation Test(순열 검정)는 대상 집합의 구분을 바꿔가며 관측한 통계량과 비교한다.
X와 Y의 단어를 모두 모으고, 현재 집합 크기를 유지한 채 두 그룹으로 다시 나눈다. 매번 S를 계산한다. 이번 예제는 6개에서 X에 넣을 3개를 고르므로 전체 경우가 6 choose 3 = 20개다. 나머지 3개가 Y가 된다.
이번 코드의 단측 p는 관측한 S 이상인 경우의 비율이다. 관측값과 동점인 경우도 포함하도록 >=를 사용한다. 원 논문 식에 쓰인 엄격한 >와 동점 처리 규칙이 다르므로 수치를 그대로 맞춰 비교할 때는 이 차이를 확인한다.
X·Y 라벨을 교환해도 된다는 귀무가설 아래에서, 지금 방향으로 이만큼 크거나 더 큰 통계량이 얼마나 나오는지 보는 것이다. ‘편향이 존재할 확률’이나 ‘결과가 우연일 확률’로 그대로 읽지는 않는다.
네 집합을 정한 뒤 검사 방향을 고르는 순서도 중요하다. 결과를 본 다음 유리한 단측 방향만 고르면 검정의 해석이 달라진다. d만 계산한 코드를 실행했다면 p값이나 통계적 유의성까지 계산한 것으로 보고하지 않는다.
7. 코사인은 벡터 한 행씩 정규화한다
여러 단어 벡터를 (단어 수, 벡터 차원) 행렬에 넣었다고 하자. 각 행이 단어 하나이므로 행마다 길이를 구하고 나누어야 한다.
행별 정규화: 각 단어 벡터 / 해당 단어 벡터의 길이
유사도 행렬: 정규화한 대상 행렬 @ 정규화한 속성 행렬의 전치
np.linalg.norm(matrix)를 축 없이 호출하면 행마다의 길이 대신 행렬 전체의 노름 하나가 나올 수 있다. 이를 모든 단어에 공통으로 나누면 의도한 코사인 계산과 달라진다.
그래서 코드에서는 axis=1, keepdims=True를 쓴다. 예를 들어 (3, 2) 행렬의 노름 결과는 (3, 1)이고 각 행에 맞게 나눌 수 있다.
한 벡터를 양수 10배 해도 방향은 그대로이므로 코사인과 WEAT 결과가 바뀌지 않아야 한다. 이런 성질을 확인하면 전체 행렬을 한꺼번에 정규화하는 오류를 발견하는 데 도움이 된다. 0벡터의 코사인은 정의되지 않으므로 계산 전에 걸러낸다.
8. 가상 벡터로 계산해보기
실제 사전 학습 모델을 내려받는 대신, 방향을 의도적으로 정한 2차원 벡터를 사용했다. 꽃·곤충이라는 이름은 집합 역할을 기억하기 위한 가상 이름이다. 실제 한국어 단어의 편향을 측정한 결과가 아니다.
X의 세 벡터는 오른쪽을, Y의 세 벡터는 왼쪽을 향하게 했다. A와 B에도 비슷한 방향 차이를 넣었다. 여섯 대상 벡터가 다른 기울기를 갖게 해서 코사인 점수의 차이도 볼 수 있게 했다.
"""가상 벡터로 WEAT 계산을 확인한다. 실제 단어 편향을 측정한 결과가 아니다."""
from itertools import combinations
import numpy as np
def unit_rows(vectors):
vectors = np.asarray(vectors, dtype=float)
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
if np.any(norms == 0):
raise ValueError('0벡터의 cosine은 정의되지 않습니다.')
return vectors / norms
def association(words, a, b):
words, a, b = map(unit_rows, (words, a, b))
return (words @ a.T).mean(axis=1) - (words @ b.T).mean(axis=1)
def weat(x, y, a, b):
sx, sy = association(x, a, b), association(y, a, b)
scores = np.concatenate([sx, sy])
spread = scores.std(ddof=0)
if spread == 0:
raise ValueError('모든 연관 점수가 같아 효과 크기를 계산할 수 없습니다.')
effect = (sx.mean() - sy.mean()) / spread
observed = sx.sum() - sy.sum()
# 같은 크기의 두 target 집합으로 나누는 모든 경우를 계산한다.
statistics = []
for indices in combinations(range(len(scores)), len(sx)):
left = np.zeros(len(scores), dtype=bool)
left[list(indices)] = True
statistics.append(scores[left].sum() - scores[~left].sum())
# 단측 검정, 관측값과 같은 경우도 센다.
p = np.mean(np.asarray(statistics) >= observed)
return sx, sy, effect, observed, p, len(statistics)
if __name__ == '__main__':
# X=가상의 꽃 3개, Y=가상의 곤충 3개.
# A=가상의 유쾌함, B=가상의 불쾌함. 방향을 의도적으로 정했다.
x = [[1, 0], [1, 1], [1, 2]]
y = [[-1, 0], [-1, 1], [-1, 2]]
a, b = [[1, 0], [1, 1]], [[-1, 0], [-1, 1]]
sx, sy, effect, statistic, p, count = weat(x, y, a, b)
print('X 연관:', np.round(sx, 4).tolist())
print('Y 연관:', np.round(sy, 4).tolist())
print('효과 크기 d:', round(float(effect), 4))
print('검정 통계량 S:', round(float(statistic), 4))
print('전체 분할 수:', count)
print('단측 p (동점 포함):', round(float(p), 4))
print('X/Y 교환 d:', round(float(weat(y, x, a, b)[2]), 4))
실행 결과를 확인했다.
X 연관: [1.7071, 1.2071, 0.7634]
Y 연관: [-1.7071, -1.2071, -0.7634]
효과 크기 d: 1.9079
검정 통계량 S: 7.3553
전체 분할 수: 20
단측 p (동점 포함): 0.05
X/Y 교환 d: -1.9079
X의 점수는 모두 양수이고 Y는 모두 음수다. 이는 직접 그렇게 방향을 정한 결과다. 효과 크기 1.9079도 그 가상 배치를 요약한 값으로 읽는다.
단측 p는 20개 분할 가운데 한 개가 관측값 이상이라 1/20=0.05다. 관측한 분할 자체도 포함했으므로 이처럼 작은 완전 열거에서는 가능한 p값의 간격이 0.05다. 정교한 언어 실험에서 작은 확률을 측정한 것과는 다르다.
마지막 줄은 X와 Y를 바꾼 결과다. 효과 크기의 크기는 같고 부호가 반대다. 이 관계도 별도로 확인했다.
9. 단어 목록이 계산의 일부다
실제 모델에 적용할 때는 네 집합을 먼저 정하고 목록을 남긴다. 결과가 크게 나오도록 몇 개씩 바꿔가며 고르면 처음의 질문도 함께 바뀐다.
어휘에 없는 단어를 제외했다면 어떤 단어가 빠졌는지와 남은 개수를 기록한다. 한 집합에서만 많이 빠지면 원래 비교하려던 개념을 충분히 대표하지 못할 수 있다. 단어가 하나도 안 남은 집합의 평균은 계산할 수 없다.
같은 단어의 중복은 그 단어에 가중치를 더 주는 효과가 생긴다. 의도한 가중인지 단순 중복인지 확인한다. X와 Y에 같은 항목을 반복해 넣거나, 서로 다른 규칙으로 토큰화한 벡터를 섞는 것도 피해야 한다.
단어의 품사, 여러 뜻, 언어, 어휘에 남기는 빈도 기준도 결과에 영향을 준다. 모델·말뭉치·단어 목록을 바꾼 실험은 조건이 달라진 실험으로 기록한다. 가까운 동의어로 일부 목록을 바꾸었을 때 방향이 유지되는지 보는 것도 결과의 안정성을 확인하는 방법이다.
10. PCA 그림은 같은 좌표계에서 그린다
X, Y, A, B를 각각 따로 pca.fit_transform()하면 네 집합마다 다른 축을 학습한다. 이렇게 나온 좌표를 한 그림에 겹치면 점 사이 거리를 비교할 기준이 맞지 않는다.
네 집합의 벡터를 한꺼번에 모아 PCA를 한 번 fit하고, 같은 변환으로 위치를 구해야 한다. 그래도 2차원으로 줄이는 동안 정보가 사라진다.
WEAT 수치는 원래 임베딩 공간에서 계산하고 그림은 탐색용으로 따로 두는 편이 해석하기 쉽다. 축소한 벡터로 계산했다면 그것도 분석 조건으로 명시한다. 그림이 분리돼 보인다는 이유만으로 원래 공간에서 큰 효과 크기가 나왔다고 가정하지 않는다.
11. 영화 시놉시스 프로젝트로 이어지는 순서
수업의 프로젝트는 영화 시놉시스로 Word2Vec을 학습한 뒤, 예술영화·일반영화와 장르 대표 단어의 연관을 비교하는 흐름이었다. 여기서는 그 절차를 복습해둔다. 이번 글에서 실제 시놉시스 학습을 실행하지는 않았다.
먼저 확보한 데이터의 출처와 전처리 기준을 확인한다. 형태소 분석에서 명사만 남기면 단어를 다루기 편해질 수 있지만, 동사·형용사가 전달하는 장르 분위기도 빠진다. 어떤 품사를 남겼는지 기록한다.
그 말뭉치로 Word2Vec을 학습하고 몇 개의 이웃 단어를 살펴본다. TF-IDF로 예술영화·일반영화나 장르의 특징적인 단어를 고를 수 있지만, 점수가 높은 단어가 항상 개념을 잘 대표하는 것은 아니다. 인물명, 제작사, 숫자, 반복된 서식도 상위에 올 수 있다.
대표 단어를 고르는 개수와 규칙을 미리 정한다. 비교 집합의 중복을 어떻게 처리할지, Word2Vec 어휘에 없는 단어를 어떻게 다룰지 정하고 최종 네 목록을 확인한다.
예를 들어 X=예술영화, Y=일반영화, A=드라마, B=액션으로 놓을 수 있다. 이 설정의 양수는 예술영화 쪽이 일반영화 쪽보다 드라마 대 액션 축에 상대적으로 더 연결되는 방향이다. 영화 자체의 좋고 나쁨을 점수화한 값은 아니다.
12. 여러 장르를 비교할 때 남길 정보
21개 장르에서 서로 다른 두 장르를 고르면 21×20/2=210쌍이다. A·B를 장르 쌍으로 바꾸고 X·Y를 고정하면 각 조합의 효과 크기를 표로 정리할 수 있다.
계산하지 않은 셀을 0으로 채우면 ‘차이가 작은 조합’처럼 보인다. 미계산·계산 불가 셀은 NaN과 마스크 등으로 따로 표시한다. 대각선과 반대쪽 삼각형을 어떻게 처리했는지도 밝힌다.
Heatmap(색상 행렬 그림)에 적을 내용은 네 집합의 역할, 행·열 순서, 양수·음수의 방향, 빈칸의 의미다. 색으로 표시한 숫자가 d라면 효과 크기라고 쓴다. p값을 계산하지 않았다면 색이 진하다는 이유로 유의하다고 표현하지 않는다.
많은 쌍의 p값을 함께 검사한다면 Multiple Testing(다중 검정)도 고려해야 한다. 이번의 작은 예제는 미리 정한 한 조합만 계산했다. 210개를 실제로 계산한 결과는 아니다.
13. 이번에 확인한 범위
확인한 것은 개별 행의 코사인, 상대 연관 점수, 효과 크기, 검정 통계량, 20개 분할의 단측 p값이다. X·Y 교환에 따른 부호 반전과 각 벡터의 양의 배율 변화에도 결과가 유지되는지도 검증했다.
효과 크기가 0에 가깝다면 지금 고른 두 대상이 두 속성에 보이는 평균 차이가 작다는 범위에서 읽는다. 다른 단어 집합이나 실제 문장·제품의 결과까지 편향이 없다고 말할 수는 없다. 반대로 큰 절댓값 하나로 실제 사용에서의 피해를 모두 설명할 수도 없다.
계산하기 전에 네 집합을 적어두고 결과 옆에 계산 조건을 남긴다. d, S, p값은 각각 답하는 질문이 다르므로 결과에 어느 값인지 표시해야 한다.
실행 환경: Python 3.12.9, NumPy 2.5.2, PyTorch 2.13.0 (CPU), SentencePiece 0.2.2, Gensim 4.4.0. 필요한 패키지는 numpy이며 python weat_example.py로 실행한다. 실제 사전 학습 벡터 다운로드와 영화 말뭉치 학습, 장르별 시각화는 실행하지 않았다.