해당 포스팅은 AIFFEL에서 제공한 학습자료를 통해 공부한 것을 정리한 것임을 밝힙니다.
학습 목표
- 데이터의 편향성에 대한 문제의식을 갖는다.
- 임베딩 모델의 편향성을 체크하는 방법 중 하나인 Word Embedding Association Test (WEAT)를 알아본다.
- WEAT 수식의 의미를 이해하고 이를 구현해본다.
- pre-train된 모델을 불러와서 WEAT score를 구해본다.
목차
- 워드 임베딩의 편향성
- WEAT를 통한 편향성 측정
- WEAT 구현하기
- 사전학습된 Word Embedding에 WEAT 적용
- 직접 만드는 Word Embedding에 WEAT 적용(1)
- 직접 만드는 Word Embedding에 WEAT 적용(2)
- 프로젝트 : 모든 장르 간 편향성 측정해 보기
참고 자료
1. 워드 임베딩의 편향성
1.1 워드 임베딩 속의 편향성
학습된 Word Embedding을 2차원으로 차원 축소하여 시각화하였을 때, 젠더 중립적인 단어임에도 불구하고 Programmer, Doctor, Engineer 등의 단어는 남성대명사 He에 가깝게, Homemaker, Nurse, Hairdresser 등의 단어는 여성대명사 She에 가깝게 위치하는 것을 확인할 수 있다.
우리가 가지고 있을지도 모르는 편견이 워드 임베딩 속 벡터들에 반영되어 있다는 것을 보여준다.
1.2 WEAT (Word Embedding Association Test)
Word Embedding Association Test는 임베딩 모델의 편향을 측정하는 방식이다.
만약 Science와 Art가 모두 젠더 중립적이라면, Word Embedding 상에서도 Science와 Art가 Male, Female의 두 단어와의 거리가 동일해야 할 것이다. 하지만 편향이 있다면 거리가 동일하지 않을 것이다.
WEAT는 Male과 Female, Science와 Art라는 개념을 가장 잘 대표하는 단어들을 여러 개 골라 단어 Set을 만든다. 단어 셋에 속한 모든 단어들끼리의 편향성을 전부 계산해서 평균을 수치화하여 편향성의 존재 여부를 밝힌다. 이러한 단어 셋을 각각 target과 attribute라고 한다.
Science를 대표하는 target 단어 셋 X, Art를 대표하는 target 단어 셋 Y가 있다고 하면 X-Y셋을 통한 개념축 하나가 얻어진다. Male을 대표하는 attribute 단어 셋 A, Female을 대표하는 attribute 단어 셋 B가 있다고 하면 A-B셋을 통한 개념축 하나가 또 얻어진다.
편향성이 없다면 X에 속한 단어들은 A에 속한 단어들과의 거리와 B에 속한 단어들과의 거리가 별 차이가 없어야 한다. (Y도 마찬가지)
WEAT score는 위와 같은 방식으로 계산된 수치로, 절댓값이 클수록 두 개념축 사이의 편향성이 크게 나타나는 것으로 해석된다.
파란색은 사람의 편향과 같은 경우이고 노란색은 사람의 편향과 반대의 경우를 의미한다.(사람이 가진 편향은 Implicit Association Tests로 측정함)
- IAT 홈페이지 : Project Implicit
위 표를 보고 알 수 있는 것은 사람이 가진 편향이 자연어 코퍼스 데이터에 반영되어 있고, 이 데이터로 만든 워드 임베딩 모델은 그 편향을 내재할 수 밖에 없다는 점이다.
2. WEAT를 통한 편향성 측정
WEAT score는 다음과 같이 정의한다.
두 벡터의 유사도를 측정하기 위해 cosine similarity(코사인 유사도)를 이용한다.
cosine similarity는 두 벡터 사이의 cosine 값을 이용하여 두 벡터의 유사도를 측정한다. 즉, 두 벡터 i, j가 주어졌을 때, cosine similarity 는 dot product와 magnitude를 사용하여 구할 수 있다.
- cos(θ) = 1 : 두 벡터의 방향이 똑같을 때
- cos(θ) = 0 : 두 벡터가 직교할 때
- cos(θ) = -1 : 두 벡터의 방향이 반대일 때
코사인 유사도는 -1 ~ 1을 가질 수 있으며, 두 벡터의 방향이 얼마나 유사한지를 나타내게 된다.
s(w, A, B)가 의미하는 것은 target에 있는 단어 w가 두 attribute 셋 (A, B)에 속한 단어들과의 유사도의 평균(mean)값이 얼마나 차이 나는지를 측정한다. 즉, s(w, A, B)는 개별단어 w가 개념축 A-B에 대해 가지는 편향성을 계산한 값이 된다.
편향성 값은 -2 ~ 2 사이의 값을 가지게 되며, 절댓값이 클수록 w는 A-B 개념축에 대해 편향성을 가진다는 뜻이다.
다시 WEAT score의 정의를 보자.
분자 부분은 target X, Y에 속하는 각 단어 x, y들이 개념축 A-B에 대해 가지는 편향성을 r각각 평균 내어 뺀 차이다. 즉 X에 속하는 단어들과 Y에 속하는 단어들이 A-B 개념축에 대해 가지는 편향성의 정도가 뚜렷이 차이날수록 WEAT score 식의 분자값의 절댓값은 커지게 된다.
이 값을 X, Y에 속하는 모든 단어들이 가지는 편향성의 값의 표준편차(std)로 normalize한 값이 최종 WEAT score가 된다.
3. WEAT 구현하기
import numpy as np
from numpy import dot
from numpy.linalg import norm
print("슝~")
두 개의 target 단어 셋 X, Y와 두 개의 attribute 단어 셋 A, B를 정의힌다.
단어 셋을 정할 때는 두 개의 target 셋의 크기가 같아야 하고, 두 개의 attribute 셋의 크기가 같아야 한다.
- targets
- X set(꽃) : 장미, 튤립, 백합, 데이지
- Y set(곤충) : 거미, 모기, 파리, 메뚜기
- attributes
- A set(유쾌) : 사랑, 행복, 웃음
- B set(불쾌) : 재난, 고통, 증오
위의 단어들의 임베딩 결과가 다음과 같다고 해보자.
target_X = {
'장미': [4.1, 1.2, -2.4, 0.5, 4.1],
'튤립': [3.1, 0.5, 3.6, 1.7, 5.8],
'백합': [2.9, -1.3, 0.4, 1.1, 3.7],
'데이지': [5.4, 2.5, 4.6, -1.0, 3.6]
}
target_Y = {
'거미': [-1.5, 0.2, -0.6, -4.6, -5.3],
'모기': [0.4, 0.7, -1.9, -4.5, -2.9],
'파리': [0.9, 1.4, -2.3, -3.9, -4.7],
'메뚜기': [0.7, 0.9, -0.4, -4.1, -3.9]
}
attribute_A = {
'사랑':[2.8, 4.2, 4.3, 0.3, 5.0],
'행복':[3.8, 3. , -1.2, 4.4, 4.9],
'웃음':[3.7, -0.3, 1.2, -2.5, 3.9]
}
attribute_B = {
'재난': [-0.2, -2.8, -4.7, -4.3, -4.7],
'고통': [-4.5, -2.1, -3.8, -3.6, -3.1],
'증오': [-3.6, -3.3, -3.5, -3.7, -4.4]
}
print("슝~")
X = np.array([v for v in target_X.values()])
Y = np.array([v for v in target_Y.values()])
print(X)
print(Y)
[[ 4.1 1.2 -2.4 0.5 4.1]
[ 3.1 0.5 3.6 1.7 5.8]
[ 2.9 -1.3 0.4 1.1 3.7]
[ 5.4 2.5 4.6 -1. 3.6]]
[[-1.5 0.2 -0.6 -4.6 -5.3]
[ 0.4 0.7 -1.9 -4.5 -2.9]
[ 0.9 1.4 -2.3 -3.9 -4.7]
[ 0.7 0.9 -0.4 -4.1 -3.9]]
A = np.array([v for v in attribute_A.values()])
B = np.array([v for v in attribute_B.values()])
print(A)
print(B)
[[ 2.8 4.2 4.3 0.3 5. ]
[ 3.8 3. -1.2 4.4 4.9]
[ 3.7 -0.3 1.2 -2.5 3.9]]
[[-0.2 -2.8 -4.7 -4.3 -4.7]
[-4.5 -2.1 -3.8 -3.6 -3.1]
[-3.6 -3.3 -3.5 -3.7 -4.4]]
s('장미', A, B) 를 계산해보자.
def cos_sim(i, j):
return dot(i, j.T)/(norm(i)*norm(j))
def s(w, A, B):
c_a = cos_sim(w, A)
c_b = cos_sim(w, B)
mean_A = np.mean(c_a, axis=-1)
mean_B = np.mean(c_b, axis=-1)
return mean_A - mean_B #, c_a, c_b
print(s(target_X['장미'], A, B))
0.6457646122337399
WEAT score값이 양수이므로, target_X에 있는 '장미'라는 단어는 attribute_B(불쾌)보다 attribute_A(유쾌)와 더 가깝다는 것을 알 수 있다.
target_Y에 있는 '거미'와 attribute_A, attribute_B와의 관계도 계산해보자.
print(s(target_Y['거미'], A, B))
-0.794002342033094
WEAT score값이 음수가 나왔으므로 '거미'는 attribute_B와 더 가깝다는 것을 알 수 있다.
target_X와 attribute_A, attribute_B 사이의 평균값, target_Y와 attribute_A, attribute_B 사이의 평균값을 구해보자.
print(s(X, A, B))
print(round(np.mean(s(X, A, B)), 3))
[0.29551989 0.51723181 0.26499096 0.50924109]
0.397
print(s(Y, A, B))
print(round(np.mean(s(Y, A, B)), 3))
[-0.44713039 -0.28310853 -0.33144878 -0.26030641]
-0.33
WEAT score의 수식 전체를 코드로 나타내보자.
def weat_score(X, Y, A, B):
s_X = s(X, A, B)
s_Y = s(Y, A, B)
mean_X = np.mean(s_X)
mean_Y = np.mean(s_Y)
std_dev = np.std(np.concatenate([s_X, s_Y], axis=0))
return (mean_X-mean_Y)/std_dev
print(round(weat_score(X, Y, A, B), 3))
1.932
WEAT score가 매우 높게 나온 것을 알 수 있다.
즉, 꽃은 유쾌한 단어와 상대적으로 가깝고, 곤충은 불쾌한 단어와 가깝다는 것을 수치적으로 확인할 수 있다.
PCA를 통해 5차원이었던 벡터를 2차원으로 줄여 그림을 그려보자.
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pc_A = pca.fit_transform(A)
pc_B = pca.fit_transform(B)
pc_X = pca.fit_transform(X)
pc_Y = pca.fit_transform(Y)
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.scatter(pc_A[:,0],pc_A[:,1], c='blue', label='A')
ax.scatter(pc_B[:,0],pc_B[:,1], c='red', label='B')
ax.scatter(pc_X[:,0],pc_X[:,1], c='skyblue', label='X')
ax.scatter(pc_Y[:,0],pc_Y[:,1], c='pink', label='Y')
파란색 점(A)과 하늘색 점(X)이 가깝고, 빨간색 점(B)과 분홍색 점(Y)이 가깝게 표현된 것을 확인할 수 있다.
4. 사전학습된 Word Embedding에 WEAT 적용
실제 pretrained된 임베딩 모델을 이용하여 계산해보자.
데이터 : GoogleNews-vectors-negative300.bin.gz
import os
data_dir = '~/aiffel/weat'
model_dir = os.path.join(data_dir, 'GoogleNews-vectors-negative300.bin')
from gensim.models import KeyedVectors
# 50만개의 단어만 활용합니다. 메모리가 충분하다면 limit 파라미터값을 생략하여 300만개를 모두 활용할 수 있습니다.
w2v = KeyedVectors.load_word2vec_format(model_dir, binary=True, limit=500000)
w2v
<gensim.models.keyedvectors.KeyedVectors at 0x7f21701cfe20>
w2v에 있는 단어 개수와 벡터 크기를 살펴보자.
# print(len(w2v.vocab)) # Gensim 3.X 버전까지는 w2v.vocab을 직접 접근할 수 있습니다.
print(len(w2v.index_to_key)) # Gensim 4.0부터는 index_to_key를 활용해 vocab size를 알 수 있습니다.
print(len(w2v['I'])) # 혹은 단어를 key로 직접 vector를 얻을 수 있습니다.
print(w2v.vectors.shape)
500000
300
(500000, 300)
w2v에는 limit로 지정한 갯수의 단어가 있고, 각 단어는 300차원을 갖는 것을 알 수 있다.
'happy'라는 단어의 벡터를 확인해보자.
w2v['happy']
array([-5.18798828e-04, 1.60156250e-01, 1.60980225e-03, 2.53906250e-02,
9.91210938e-02, -8.59375000e-02, 3.24218750e-01, -2.17285156e-02,
.....
-7.32421875e-02, -3.54003906e-02, 8.88671875e-02, -3.02734375e-01],
dtype=float32)
'happy'와 가장 유사한 단어를 확인해보자.
w2v.most_similar(positive=['happy'])
[('glad', 0.7408890724182129),
('pleased', 0.6632170677185059),
('ecstatic', 0.6626912355422974),
('overjoyed', 0.6599286794662476),
('thrilled', 0.6514049172401428),
('satisfied', 0.6437949538230896),
('proud', 0.636042058467865),
('delighted', 0.627237856388092),
('disappointed', 0.6269949674606323),
('excited', 0.6247665286064148)]
다른 단어들도 확인해보자.
w2v.most_similar(positive=['family'])
[('relatives', 0.6662653088569641),
('familiy', 0.6517067551612854),
('families', 0.6252894997596741),
('siblings', 0.6140849590301514),
('friends', 0.6128394603729248),
('mother', 0.6065612435340881),
('aunt', 0.5811319947242737),
('grandparents', 0.5762072205543518),
('father', 0.5717043876647949),
('Family', 0.5672314763069153)]
w2v.most_similar(positive=['school'])
[('elementary', 0.7868632078170776),
('schools', 0.7411909103393555),
('elementary_schools', 0.6597153544425964),
('kindergarten', 0.6529811024665833),
('eighth_grade', 0.6488089561462402),
('School', 0.6477997303009033),
('teacher', 0.63824063539505),
('students', 0.6301522850990295),
('classroom', 0.6281620264053345),
('Schools', 0.6172096133232117)]
나름 해당 모델이 단어의 의미를 담은 벡터로 변환이 잘 되었다고 생각한다.
이제 WEAT를 통해 이 모델의 편향성을 확인해보자. (논문에 있던 단어셋으로 구성)
target_X = ['science', 'technology', 'physics', 'chemistry', 'Einstein', 'NASA', 'experiment', 'astronomy']
target_Y = ['poetry', 'art', 'Shakespeare', 'dance', 'literature', 'novel', 'symphony', 'drama']
attribute_A = ['brother', 'father', 'uncle', 'grandfather', 'son', 'he', 'his', 'him']
attribute_B = ['sister', 'mother', 'aunt', 'grandmother', 'daughter', 'she', 'hers', 'her']
X = np.array([w2v[word] for word in target_X])
Y = np.array([w2v[word] for word in target_Y])
A = np.array([w2v[word] for word in attribute_A])
B = np.array([w2v[word] for word in attribute_B])
weat_score(X, Y, A, B)
1.2624874
과학과 관련된 단어가 남성과 관련된 단어와 가깝고, 예술과 관련된 단어가 여성과 관련된 단어와 가깝게 나타났다. 많은 사람이 가진 편향이 임베딩 모델에 반영되었다고 볼 수 있다.
다른 데이터셋으로 모델의 편향성을 확인해보자.
target_X = ['pizza', 'coke', 'hamburger', 'ham', 'ramen', 'icecream', 'candy']
target_Y = ['salad', 'fruit', 'vegetable', 'herb', 'root', 'greens', 'wholesome']
attribute_A = ['junk', 'canned', 'convenience', 'frozen', 'fast']
attribute_B = ['health', 'beneficial', 'good', 'nourishing', 'nutritious']
X = np.array([w2v[word] for word in target_X])
Y = np.array([w2v[word] for word in target_Y])
A = np.array([w2v[word] for word in attribute_A])
B = np.array([w2v[word] for word in attribute_B])
weat_score(X, Y, A, B)
1.6909266
인스턴트 식품의 예시와 인스턴트를 의미하는 단어가 가까운 것은 당연하기에 이 경우, 모델이 편향되어있다기보다 단어의 의미를 잘 파악했다고 볼 수 있다.
동일한 target셋에 다른 attribute셋을 만들어 편향성을 확인해보자.
target_X = ['pizza', 'coke', 'hamburger', 'ham', 'ramen', 'icecream', 'candy']
target_Y = ['salad', 'fruit', 'vegetable', 'herb', 'root', 'greens', 'wholesome']
attribute_A = ['book', 'essay', 'dictionary', 'magazine', 'novel']
attribute_B = ['news', 'report', 'statement', 'broadcast', 'word']
X = np.array([w2v[word] for word in target_X])
Y = np.array([w2v[word] for word in target_Y])
A = np.array([w2v[word] for word in attribute_A])
B = np.array([w2v[word] for word in attribute_B])
weat_score(X, Y, A, B)
-0.05137869
0에 가까운 결과가 나왔다. 임베딩 모델이 판단하기에 어느 것과도 가깝다고 할 수 없기 때문이다.
이제 주어진 데이터로 다음과 같은 과정을 수행해보자.
- 형태소 분석기를 이용하여 품사가 명사인 경우, 해당 단어를 추출하기
- 추출된 결과로 embedding model 만들기
- TF/IDF로 해당 데이터를 가장 잘 표현하는 단어 셋 만들기
- embedding model과 단어 셋으로 WEAT score 구해보기
Step 1. 형태소 분석기를 이용하여 품사가 명사인 경우, 해당 단어를 추출하기
synopsis.txt에는 2001년부터 2019년 8월까지 제작된 영화들의 시놉시스 정보가 있다. synopsis.txt의 일부를 읽어보자.
import os
with open(os.getenv('HOME')+'/aiffel/weat/synopsis.txt', 'r') as file:
for i in range(20):
print(file.readline(), end='')
사운드 엔지니어 상우(유지태 분)는 치매에 걸린 할머니(백성희 분)와
젊은 시절 상처한 한 아버지(박인환 분), 고모(신신애 분)와 함께 살고 있다.
어느 겨울 그는 지방 방송국 라디오 PD 은수(이영애 분)를 만난다.
자연의 소리를 채집해 틀어주는 라디오 프로그램을 준비하는 은수는 상우와 녹음 여행을 떠난다.
....
이제 품사가 명사인 경우만 남겨 tokenized라는 변수명으로 저장해보자.
KoNLPy 패키지를 이용해보자.
# 약 15분정도 걸립니다.
from konlpy.tag import Okt
okt = Okt()
tokenized = []
with open(os.getenv('HOME')+'/aiffel/weat/synopsis.txt', 'r') as file:
while True:
line = file.readline()
if not line: break
words = okt.pos(line, stem=True, norm=True)
res = []
for w in words:
if w[1] in ["Noun"]: # "Adjective", "Verb" 등을 포함할 수도 있습니다.
res.append(w[0]) # 명사일 때만 tokenized 에 저장하게 됩니다.
tokenized.append(res)
print("슝~")
print(len(tokenized))
71156
총 71,156개의 명사가 있다.
Step 2. 추출된 결과로 embedding model 만들기
from gensim.models import Word2Vec
# tokenized에 담긴 데이터를 가지고 나만의 Word2Vec을 생성합니다. (Gensim 4.0 기준)
model = Word2Vec(tokenized, vector_size=100, window=5, min_count=3, sg=0)
model.wv.most_similar(positive=['영화'])
# Gensim 3.X 에서는 아래와 같이 생성합니다.
# model = Word2Vec(tokenized, size=100, window=5, min_count=3, sg=0)
# model.most_similar(positive=['영화'])
[('작품', 0.891692578792572),
('다큐멘터리', 0.8387867212295532),
('드라마', 0.8179060816764832),
('영화로', 0.8052785992622375),
('소재', 0.7913830876350403),
('코미디', 0.7907821536064148),
('형식', 0.7798620462417603),
('감동', 0.7791918516159058),
('주제', 0.7722824215888977),
('스토리', 0.7703076601028442)]
Word2Vec이 잘 훈련된 것 같은지 다른 단어로도 확인해보자.
model.wv.most_similar(positive=['사랑'])
[('가슴', 0.7009817361831665),
('첫사랑', 0.6973416209220886),
('진심', 0.6958594918251038),
('연애', 0.6941562294960022),
('종이학', 0.6915178298950195),
('행복', 0.6862002015113831),
('정일', 0.6858452558517456),
('이별', 0.6715431213378906),
('애정', 0.6711853742599487),
('고백', 0.6703489422798157)]
model.wv.most_similar(positive=['연극'])
[('영감', 0.8767934441566467),
('시나리오', 0.8762630224227905),
('영화감독', 0.8708407282829285),
('대본', 0.8701880574226379),
('배우', 0.8672288656234741),
('연기', 0.8634957075119019),
('데뷔', 0.8601243495941162),
('캐스팅', 0.8595213294029236),
('무용', 0.8583064079284668),
('예술', 0.8562657833099365)]
전반적으로 유사한 단어들이 출력된 것으로 보아 Word2Vec이 잘 훈련된 것 같다.
Step 3. TF-IDF로 해당 데이터를 가장 잘 표현하는 단어 셋 만들기
WEAT score를 구할 때, targets_X, targets_Y, attribute_A, attribute_B 단어 셋을 만들어주어야 한다. 이제 두 축을 어떤 기준으로 잡고, 해당 축의 어떤 항목을 사용할지 정해야 한다.
여기서 두 축으로 영화 장르, 영화 구분 정보(일반영화, 예술영화, 독립영화)를 이용해보자.
- 영화 구분
- synopsis_art.txt : 예술영화
- synopsis_gen.txt : 일반영화(상업영화)
- 그 외는 독립영화 등으로 분류됩니다.
- 장르 구분
- synopsis_SF.txt: SF
- synopsis_가족.txt: 가족
- synopsis_공연.txt: 공연
- synopsis_공포(호러).txt: 공포(호러)
- synopsis_기타.txt: 기타
- synopsis_다큐멘터리.txt: 다큐멘터리
- synopsis_드라마.txt: 드라마
- synopsis_멜로로맨스.txt: 멜로로맨스
- synopsis_뮤지컬.txt: 뮤지컬
- synopsis_미스터리.txt: 미스터리
- synopsis_범죄.txt: 범죄
- synopsis_사극.txt: 사극
- synopsis_서부극(웨스턴).txt: 서부극(웨스턴)
- synopsis_성인물(에로).txt: 성인물(에로)
- synopsis_스릴러.txt: 스릴러
- synopsis_애니메이션.txt: 애니메이션
- synopsis_액션.txt: 액션
- synopsis_어드벤처.txt: 어드벤처
- synopsis_전쟁.txt: 전쟁
- synopsis_코미디.txt: 코미디
- synopsis_판타지.txt: 판타지
예술영화와 일반영화(상업영화)라는 영화 구분을 target으로 삼고, 드라마 장르와 액션 장르라는 장르 구분을 attribute로 삼아 WEAT score를 계산해보자. (드라마 장르에는 예술영화적 성격이 강하고, 액션 장르에는 일반 영화적 성격이 강할 것이라는 편향성이 워드 임베딩 상에 얼마나 나타나고 있는지를 측정)
'synopsis_art.txt'(예술영화), 'synopsis_gen.txt'(일반영화(상업영화)) 두 파일을 읽어보자.
import os
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
from konlpy.tag import Okt
art_txt = 'synopsis_art.txt'
gen_txt = 'synopsis_gen.txt'
def read_token(file_name):
okt = Okt()
result = []
with open(os.getenv('HOME')+'/aiffel/weat/'+file_name, 'r') as fread:
print(file_name, '파일을 읽고 있습니다.')
while True:
line = fread.readline()
if not line: break
tokenlist = okt.pos(line, stem=True, norm=True)
for word in tokenlist:
if word[1] in ["Noun"]:#, "Adjective", "Verb"]:
result.append((word[0]))
return ' '.join(result)
print("슝~")
품사가 명사인 경우만 추출하여 art, gen 변수에 할당해보자.
art = read_token(art_txt)
gen = read_token(gen_txt)
WEAT 계산을 위해서 총 4개의 단어셋 X, Y, A, B가 필요하다. 각 단어셋을 가장 잘 대표할 수 있는 단어들을 선정해야 한다. 꼭 정해진 방법은 있지 않으나, 잘 대표하는 단어가 다른 데이터에서는 자주 등장하지 않는 것이 적당할 것이다.
그렇기에 코퍼스에서 자주 나타나는(TF가 높은) 단어지만, 다른 코퍼스에까지 두루 걸쳐 나오지 않는(IDF가 높은) 단어를 선정하는 TF-IDF 방식을 사용해보자. (해당 방식이 최선이라는 것은 아님!)
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform([art, gen])
print(X.shape)
(2, 41082)
print(vectorizer.vocabulary_['영화'])
print(vectorizer.get_feature_names()[23976])
23976
영화
m1 = X[0].tocoo() # art를 TF-IDF로 표현한 sparse matrix를 가져옵니다.
m2 = X[1].tocoo() # gen을 TF-IDF로 표현한 sparse matrix를 가져옵니다.
w1 = [[i, j] for i, j in zip(m1.col, m1.data)]
w2 = [[i, j] for i, j in zip(m2.col, m2.data)]
w1.sort(key=lambda x: x[1], reverse=True) #art를 구성하는 단어들을 TF-IDF가 높은 순으로 정렬합니다.
w2.sort(key=lambda x: x[1], reverse=True) #gen을 구성하는 단어들을 TF-IDF가 높은 순으로 정렬합니다.
print('예술영화를 대표하는 단어들:')
for i in range(100):
print(vectorizer.get_feature_names()[w1[i][0]], end=', ')
print('\n')
print('일반영화를 대표하는 단어들:')
for i in range(100):
print(vectorizer.get_feature_names()[w2[i][0]], end=', ')
예술영화를 대표하는 단어들:
그녀, 자신, 시작, 위해, 사랑, 사람, 영화, 친구, 남자, 가족, 이야기, 마을, 사건, 마음, 세상.....
일반영화를 대표하는 단어들:
자신, 그녀, 영화제, 위해, 사람, 시작, 국제, 영화, 친구, 사랑, 남자, 이야기......
두 개념을 대표하는 단어를 TF-IDF가 높은 순으로 추출하고 싶었으나, 양쪽에 중복된 단어가 많은 것을 확인할 수 있다. 두 개념축이 대조되도록 대표하는 단어셋을 만들어야하기에 단어가 서로 중복되지 않게 단어셋을 추출해야한다. 우선 상위 100개의 단어 중 중복되는 단어를 제외하고 상위 n(=15)개의 단어를 추출해보자.
n = 15
w1_, w2_ = [], []
for i in range(100):
w1_.append(vectorizer.get_feature_names()[w1[i][0]])
w2_.append(vectorizer.get_feature_names()[w2[i][0]])
# w1에만 있고 w2에는 없는, 예술영화를 잘 대표하는 단어를 15개 추출한다.
target_art, target_gen = [], []
for i in range(100):
if (w1_[i] not in w2_) and (w1_[i] in model.wv): target_art.append(w1_[i])
if len(target_art) == n: break
# w2에만 있고 w1에는 없는, 일반영화를 잘 대표하는 단어를 15개 추출한다.
for i in range(100):
if (w2_[i] not in w1_) and (w2_[i] in model.wv): target_gen.append(w2_[i])
if len(target_gen) == n: break
이렇게 추출된 단어를 확인해보자.
print(target_art)
['아빠', '음악', '결심', '운명', '지금', '여인', '이름', '이후', '준비', '감정', '만난', '처음', '충격', '누구', '그린']
print(target_gen)
['서울', '애니메이션', '여성', '가지', '주인공', '대해', '연출', '사회', '다큐멘터리', '부문', '섹스', '바로', '의도', '계획', '정체']
이번에는 장르 별 대표 단어를 추출해보자.
드라마 장르와 액션 장르를 다루어 보려고 하나 2개의 장르만 고려하기 보다는 여러 장르의 코퍼스를 두루 고려하는 것이 특정 장르를 대표하는 단어를 선택하는 게 더 유리할 것이다.
총 6개의 주요 장르만을 고려해보자.
genre_txt = ['synopsis_drama.txt', 'synopsis_romance.txt', 'synopsis_action.txt', 'synopsis_comedy.txt', 'synopsis_war.txt', 'synopsis_horror.txt']
genre_name = ['드라마', '멜로로맨스', '액션', '코미디', '전쟁', '공포(호러)']
print("슝~")
품사가 명사인 경우만을 추출하여 genre 변수에 할당해보자.
genre = []
for file_name in genre_txt:
genre.append(read_token(file_name))
TF-IDF 방식으로 단어 셋을 구성해보자.
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(genre)
print(X.shape)
(6, 33151)
m = [X[i].tocoo() for i in range(X.shape[0])]
w = [[[i, j] for i, j in zip(mm.col, mm.data)] for mm in m]
for i in range(len(w)):
w[i].sort(key=lambda x: x[1], reverse=True)
attributes = []
for i in range(len(w)):
print(genre_name[i], end=': ')
attr = []
j = 0
while (len(attr) < 15):
if vectorizer.get_feature_names()[w[i][j][0]] in model.wv:
attr.append(vectorizer.get_feature_names()[w[i][j][0]])
print(vectorizer.get_feature_names()[w[i][j][0]], end=', ')
j += 1
attributes.append(attr)
print()
드라마: 자신, 영화제, 그녀, 사람, 사랑, 위해, 영화, 시작
멜로로맨스: 그녀, 사랑, 자신, 시작, 남자, 남편, 여자
액션: 위해, 자신, 시작, 조직, 사건, 사람, 그녀, 경찰, 전쟁
전쟁: 전쟁, 위해, 전투, 시작, 작전, 독일군, 부대, 독일, 윈터스
공포(호러): 시작, 위해, 사람, 자신, 친구, 그녀, 사건, 공포, 발견, 죽음
각 장르를 대표하는 단어들을 추출해보았다. 중복된 것이 종종 있지만 art, gen 두 개의 단어 셋을 추출했을 때에 비해 적다. 그래서 중복을 삭제하지 않고 그대로 사용하자.
Step 4. embedding model과 단어 셋으로 WEAT score 구해보기
target_X는 art, target_Y는 gen, attribute_A는 '드라마', attribute_B는 '액션'과 같이 정할 수 있다.
target_X는 art, target_Y는 gen으로 고정, attribute_A, attribute_B를 바꿔가면서 구해보자. 구한 결과를 21x21 매트릭스 형태로 표현해서 matrix라는 변수에 담아보자.
matrix = [[0 for _ in range(len(genre_name))] for _ in range(len(genre_name))]
print("슝~")
X = np.array([model.wv[word] for word in target_art])
Y = np.array([model.wv[word] for word in target_gen])
for i in range(len(genre_name)-1):
for j in range(i+1, len(genre_name)):
A = np.array([model.wv[word] for word in attributes[i]])
B = np.array([model.wv[word] for word in attributes[j]])
matrix[i][j] = weat_score(X, Y, A, B)
print("슝~")
matrix를 채워보았다. WEAT score값을 보고, 우리의 직관과 비슷한지 확인해보자.
for i in range(len(genre_name)-1):
for j in range(i+1, len(genre_name)):
print(genre_name[i], genre_name[j],matrix[i][j])
드라마 멜로로맨스 -0.8363403
드라마 액션 0.02806083
드라마 코미디 0.093134105
드라마 전쟁 0.46124673
드라마 공포(호러) -0.1326768
멜로로맨스 액션 0.7847887
멜로로맨스 코미디 0.9135294
멜로로맨스 전쟁 0.80272585
멜로로맨스 공포(호러) 0.7815207
액션 코미디 0.0024755013
액션 전쟁 0.5600446
액션 공포(호러) -0.24756423
코미디 전쟁 0.48531514
코미디 공포(호러) -0.27205512
전쟁 공포(호러) -0.540005
WEAT score가 0.8 이상, -0.8 이하인 경우만 해석해보자.
1) 드라마 멜로로맨스 -0.8363403
예술영화와 일반영화, 그리고 드라마와 멜로로맨스의 WEAT score의 의미를 해석해보면, 예술영화는 멜로로맨스, 일반영화는 드라마와 가깝다고 볼 수 있다. (부호가 마이너스이므로 사람의 편향과 반대라는 것을 알 수 있다.)
2) 멜로로맨스 코미디 0.9135294
예술 영화는 멜로로맨스에 가깝고 코미디는 일반 영화와 가깝다고 볼 수 있다.
3) 멜로로맨스 전쟁 0.80272585
예술 영화는 멜로로맨스에 가깝고, 전쟁은 일반 영화와 가깝다고 볼 수 있다.
▶ 예술 영화 : 멜로로맨스 / 일반영화 : 드라마, 코미디, 전쟁
import numpy as np;
import seaborn as sns;
np.random.seed(0)
# 한글 지원 폰트
sns.set(font='NanumGothic')
# 마이너스 부호
plt.rcParams['axes.unicode_minus'] = False
ax = sns.heatmap(matrix, xticklabels=genre_name, yticklabels=genre_name, annot=True, cmap='RdYlGn_r')
ax
'AIFFEL > Going Deeper(NLP)' 카테고리의 다른 글
[Going Deeper(NLP)] 9. Transformer가 나오기까지 (0) | 2022.04.06 |
---|---|
[Going Deeper(NLP)] 7. Seq2seq와 Attention (0) | 2022.03.30 |
[Going Deeper(NLP)] 5. 워드 임베딩 (0) | 2022.03.28 |
[Going Deeper(NLP)] 3. 텍스트의 분포로 벡터화 하기 (0) | 2022.03.23 |
[Going Deeper(NLP)] 2. 멋진 단어사전 만들기 (0) | 2022.03.17 |