for textmining

한국어의 주어와 주제

|

이번 글에서는 한국어의 주어와 주제에 대해 살펴보도록 하겠습니다. 이번 글은 고려대 정연주 선생님 강의와 ‘한국어문법총론1(구본관 외 지음, 집문당 펴냄)’을 정리했음을 먼저 밝힙니다. 그럼 시작하겠습니다.

문장의 구조를 보는 두 가지 관점

문장의 구조는 단 한 가지의 관점으로만 설명할 수 있는 것은 아닙니다. 다양한 관점에서 여러 가지 형식의 문장 구조를 상정할 수 있는데, 그중 한국어 문장의 구조를 설명하는 데 가장 유용한 관점은 크게 서술어를 중심으로 파악한 통사구조(syntactic structure)와 정보 전달의 방식을 중심으로 파악한 정보구조(information structure)로 나누어 볼 수 있습니다.

통사구조

전자의 관점은 서술어와 주어 사이의 문법적 관계에 주목합니다. 통사구조의 정확한 정의는 다음과 같습니다.

서술어가 요구하는 성분의 수와 종류의 정보에 따라 구축된 문형과 여러 통사적 원리에 따라 부가된 성분이 형성한 구조

‘진이는 밥을 먹어’라는 문장을 이 관점에 입각해 살펴보면 다음과 같이 분석할 수 있습니다.

[진이는 [밥을] 먹어]]]

주어 + 목적어 + 서술어

여기에서 자세히 살펴보면 ‘목적어-서술어’가 ‘주어-서술어’보다 더 긴밀한 관계를 맺고 있다는 걸 알 수 있습니다. 서술어가 가리키는 행위(食)가 지속될 수록 대상(rice)은 변화하게 마련이고, 주체가 그 행위를 마치는 순간 대상은 사라집니다. 하지만 주체(진이)는 행위가 종료되더라도 존재합니다. 즉 이 문장에서 서술어는 주어보다는 목적어와 의미적으로 더 큰 관련을 갖고 있다는 것이지요.

이 때문에 [먹어]라는 동사는 [밥을]이라는 명사구(Noun Phrase)와 우선 결합하고, 이후 [밥을 먹어]라는 동사구(Verb Phrase)에 [진이는]이라는 명사구가 결합했다고 보는 것이 자연스럽습니다.

정보구조

후자의 관점은 문장 내 요소들이 얼마나 ‘정보성’을 가지고 있는지에 주목합니다. 정보구조의 정의는 다음과 같습니다.

문장에서 정보 전달의 대상(구정보)과 그 주제에 대해 언급하는 내용(신정보)이 실현되는 방식의 구조

정보구조 관점에서 문장 구조를 분석해보겠습니다.

A: 진이는 뭐해?

B: [진이는] [밥을 먹어]

B에서 [진이는]은 질문한 사람도, 대답하는 사람도 모두 알고 있는 ‘구정보’입니다. 반면 [밥을 먹어]는 질문자는 모르는 ‘신정보’입니다.

구정보, 신정보

그렇다면 ‘정보가 새롭다’는 건 구체적으로 어떤 의미일까요? 크게 두 가지 기준이 있습니다. 첫째는 ‘언어표현에 대응되는 지시대상(referent)이 청자의 마음 속에서 얼마나 친숙한가’를 놓고 따집니다. 해당 요소가 청자의 마음 속에서 활성화되어 있다면 (지시적)구정보, 화자의 발화를 듣고서야 비로소 활성화되기 시작했다면 (지시적)신정보입니다.

둘째 기준은 문장 내 구조를 놓고 따지는 겁니다. 문장은 대개 $X$와 $Y$ 두 부분으로 나뉘는데, 이 문장은 $X$에 대한 것이고 $Y$는 $X$에 관한 정보를 제공하는 부분입니다. 이 때 $X$는 $Y$와의 관계 속에서 구정보이고, $Y$는 $X$에 대하여 단언되거나 질문되는 새로운 정보입니다. $X$를 주제(topic) 또는 (관계적)구정보, $Y$를 평언(comment) 또는 (관계적)신정보라고 합니다.

이 글에서는 용어를 다음과 같이 쓰겠습니다. 지시적인 구정보/신정보를 언급할 때는 아래 용어들과는 별도로 구분할 예정입니다.

구정보=관계적 구정보=주제(topic)

신정보=관계적 신정보=평언(comment)

한국어에서 극단적으로는 다음 예문처럼 문장 전체가 신정보로만 되어 있는 경우도 있습니다. 정보구조 관점에선 B 문장 전체를 한 단위로 분석합니다. B처럼 구정보와 신정보로 나눌 수 없는 문장을 ‘제언문’이라 하며, 한국어에서는 제언문의 주어가 ‘이/가’로 실현됩니다.

A: 무슨 일이야?

B: [진이가 넘어졌어]

다른 예문을 보겠습니다.

[비가 온다]

위 문장은 ‘비’에 대해 어떤 정보를 언급한다고 하기보다는 비가 온다는 정보 전체를 통째로 언급하는 문장입니다. 위 예문에 주제는 명시돼있지 않지만, 굳이 말하자면 ‘기상 현상’입니다. 위 예문은 기상 현상에 대해 말하는 문장이라는 이야기입니다.

이를 ‘주제+평언’ 구조로 바꾸려면 아래 예문과 같이 한국어에서 주제를 나타내는 데 전형적으로 쓰이는 표지(marker)인 보조사 ‘-은/는’을 반드시 써야 합니다. 아래 예문에서 ‘비는’이 주제, ‘온다’가 평언이 됩니다.

[비는] [온다]

한국어에서는 화자가 어떤 대상에 대해 어떤 사실을 언급하려고 한다면, 그 대상은 모두 주제라고 말할 수 있습니다. 예문을 보겠습니다.

(가) [영희는] [착해요]

(나) [순희도] [착해요]

(가)의 ‘영희는’은 화자가 언급하려고 하는 대상이므로 주제입니다. 그런데 (나)에서는 ‘-은/는’이 결합되지 않은 ‘순희도’가 주제라고 할 수 있습니다. 다만 ‘순희도’에서는 보조사 ‘-도’가 지닌 also/too의 의미가 덧붙었을 뿐입니다. 다시 말해 주제는 정보구조적 ‘의미’로 파악하는 것이지 표지 ‘-은/는’으로 파악하는 것이 아니라는 이야기입니다.

주제의 종류

인가주제(ratified topic)이란 이미 이전 담화에서 주제로 확립되어 있는 요소를 가리킵니다. 관계적으로나 지시적으로나 구정보에 해당합니다. 한국어에서 인가주제는 ‘은/는’으로 표시되거나 아예 생략되는 일이 많습니다. 아래 예문에서 ‘아이들은’이 인가주제입니다.

A: 아이들은 뭐해?

B: (아이들은) 밥 먹어.

비인가주제(unratified topic)란 담화에서 현재 발화에 의해 새로 도입되어 이제야 막 주제로서 확립된 요소를 나타냅니다. 관계적으로는 구정보이나, 지시적으로는 신정보에 해당합니다. (1)처럼 ‘명사+말이야/말이에요/말입니다/말씀입니다/말인데요/말씀인데요 등’으로 실현되거나 (2)처럼 ‘있잖아(요)/있지/있죠/요’로 실현됩니다. 아래 예문에서 ‘박 사장’이 비인가주제입니다.

(1) 박 사장 말이야, 정말 열 받게 만든다. 나를 일하는 기계로 생각하나 봐.

(2) 박 사장 있잖아요, 어제 고혈압으로 쓰러졌대요.

대조주제(contrastive topic)는 주제이되 둘 이상이 서로 대조를 이루는 것을 말합니다. 한국어에서 대조주제는 ‘은/는’으로 실현됩니다. 다음 예문과 같습니다.

A: 영이랑 순이 출산했니?

B: 응, 영이는 아들을 낳았고, 순이는 딸을 낳았어.

초점

초점(focus)은 문장이 나타내는 정보의 핵심을 나타냅니다. 주제와 짝을 이루는 평언 전체가 초점일 수도 있고, 평언 내부의 특정 성분만이 초점일 수도 있습니다. 예문을 보겠습니다.

A: 브라질은 어떤 나라야?

B: 브라질은 [축구를 잘해].

B에서 주제는 ‘브라질은’이고 평언은 ‘축구를 잘해’입니다. 하지만 초점은 ‘축구를 잘해’라는 평언 전체입니다. 다른 예문을 보겠습니다.

C: 축구 어디가 잘하지?

D: 축구는 [브라질이 잘해].

D에서 주제는 ‘축구는’이고 평언은 ‘브라질이 잘해’입니다. 하지만 초점은 ‘브라질이’라는 평언의 일부 성분입니다.

초점의 종류

정보초점(informational focus)이란 주제에 대해 제공되는 정보를 가리킵니다. 한국어에서 정보초점은 ‘이/가’로 실현됩니다.

A: 이거 누가 만들었니?

B: 진이가 만들었어.

대조초점(contrastive focus)이란 초점 요소와 대안 집합의 다른 원소들 사이에 대조가 부각되는 경우 해당 초점을 가리킵니다. 여기에서 대안집합이란 어떤 요소가 나타난 위치에 올 수 있는 여러 후보들의 집합입니다. 한국어에서 대조초점은 ‘은/는’이나 ‘이/가’로 실현됩니다.

A: 철수 데리러 엄마가 왔어 아빠가 왔어? B: 엄마가 왔어.

A: 이번에 시험 친 친구들 중 누가 합격했니? B: 철수는 합격했어.

주어의 정보적 역할에 따른 표시방식

통사 구조상 주어로 분석될 수 있는 문장성분도 정보구조 관점에서 보면 정보적 역할이 문장마다 달라질 겁니다. 주어의 정보적 역할에 따른 표시방식을 표로 정리하면 다음과 같습니다.

은/는 이/가
인가 주제 정보 초점
대조 주제 대조 초점
대조 초점 제언문의 주어

한편 정보구조상의 주제가 통사 구조상의 주어와 일치하면 일반적으로 주제 표지 ‘-은/는’만을 사용하고 주어 표지 ‘-이/가’는 쓰지 않는다고 합니다.

주어중심언어

주어는 ‘무엇이 어찌하다’, ‘무엇이 어떠하다’, ‘무엇이 무엇이다’와 같은 문장에서 ‘무엇이’에 해당하는 걸 말합니다. 즉 주어는 문장의 통사구조에서 문장이 나타내는 행위/작용의 주체, 상태/성질이나 정체 밝힘 등의 대상이 언어적으로 나타난 것을 가리킵니다. 한국어에서 주어는 일반적으로 주격조사 ‘-이/가’가 붙어 표시됩니다.

문법적인 주어가 문장 구성의 중심적인 역할을 하는 언어를 ‘주어중심언어’라고 합니다. 영어가 대표적인 사례입니다. 주어가 생략되기 어려우며 주어가 의미적으로 굳이 필요 없는 경우에도 허사주어를 반드시 써야 합니다. 다음과 같습니다.

It rains (비가 온다)

주제중심언어

한국어는 ‘주제’도 중요하고 ‘주어’도 중요한 주제-주어 동시 부각형 언어(topic-subject prominent language)라고 합니다. 한국어에서 전형적인 주제 표지인 ‘-은/는’이 표시되어 문두에 나타난 예시를 몇 가지 살펴보겠습니다.

(ㄱ) 그 책은 나도 읽어 봤어

(ㄴ) 어제는 하루 종일 집에서 쉬었어

(ㄷ) 진이는 내가 벌써 저녁을 사줬어

(ㄹ) 향기는 장미가 더 좋지

(ㅁ) 읽기는 아무래도 이 책이 더 쉽다

위 예시의 볼드 표시 어절은 모두 통사구조상의 주어로 보기는 어렵습니다. (ㄱ)만 예로 들면 ‘그 책은’은 통사구조상 목적어로 쓰였습니다. 다른 예를 살펴볼까요?

(ㅂ) (음식점에서 주문할 때) 저는 짜장면이요

(ㅂ)에서 ‘나는’은 ‘짜장면(이다)’의 주어가 아니라는 점을 분명하게 알 수 있습니다. 말하는 짜장면이 있을리가 없잖아요. 이 때 ‘저는’은 주제에 해당하며 실제 주어는 상황 맥락에 따라 생략됐다고 분석하는 것이 매끄럽습니다. (ㅂ)을 통사구조로 분석해 ‘저는’의 문장성분을 굳이 따지자면 부사어에 해당할 것입니다. ‘나는’을 생략해도 문장이 성립한다는 점을 감안하면 서술어가 요구하는 필수부사어가 아니라는 사실 또한 알 수 있습니다.

주제-주어 동시 부각형 언어인 한국어의 통사적 특징을 몇 가지 살펴보겠습니다.

(1) ‘주제+평언’ 관계에 기초해 조직되는 문장이 많습니다.

(2) 주제가 될 수 있는 성분에 대한 제약이 적습니다.

(3) 주제가 문장 구성에서 중심적인 역할을 합니다.

(4) 주제는 표면상 일정하게 표시됩니다.

(5) 주어는 생략 가능하고, 허사주어가 필요 없습니다.

(6) 이중주어 구문이 발달했습니다.

이중주어 문제

한국어에서 무엇을 주어로 해야할지에 대해서는 매우 다양한 이견이 존재한다고 합니다. 대개 아래 예문과 같은 이중주어문의 해석과 관련이 있습니다.

(1) 누나는 눈이 크다.

(1)은 정보구조 관점에서 ‘주제+주어+서술어’로 분석할 수도 있고, 통사구조 관점에서 ‘주어+[주어+서술어]’로 분석할 수도 있습니다. 후자의 분석에서 [주어+서술어] 형태인 ‘눈이 크다’는 전체 문장의 서술어로서 절의 형식을 띠고 있으므로 서술절이라고 부릅니다.

다른 예문을 보겠습니다.

(2) 나는 호랑이가 무섭다.

(2)도 (1)처럼 정보구조 관점에서 ‘주제+주어+서술어’로 분석할 수도 있고, 통사구조 관점에서 ‘주어+[주어+서술어]’로 분석할 수도 있습니다. 하지만 여기에서 후자의 분석은 문제가 있습니다.

‘무섭다’의 대상은 ‘호랑이’이고 ‘나’는 ‘무섭다’라는 심리상태를 경험하는 사람입니다. 그런데 일반적으로 형용사는 그 성질이나 상태를 지니는 대상이 주어이므로, (2)에서 ‘나는’은 주어가 될 수 없습니다.

이때 ‘나는’은 바로 주제가 됩니다. 다시 말해 화자는 ‘나’라는 대상에 어떤 정보(호랑이가 무섭다)를 전달하고 있다고 분석하는 것이 매끄럽다는 이야기입니다.

그런데 통사구조 관점에서 문장을 분석할 때 ‘나는’이 주어가 아니라면, ‘나는’은 대체 어떤 문장성분(sentence component)인가 하는 의문이 제기될 수 있습니다. 그런데 정보구조상 주제는 서술어가 요구하는 문장성분이 아닌 경우도 많다는 점에 유의할 필요가 있습니다. 통사구조와 정보구조는 엄밀히 말하면 별개의 개념이기 때문입니다. 따라서 통사구조 관점에서 ‘나는’의 문장 성분을 굳이 따진다면 부사어 정도로 보아야 할 것입니다.

이의 연장선상에서 아래와 같은 문장은 통사구조보다는 정보구조 틀로 설명하는 것이 편리합니다. 아래 예시에서 ‘꽃은’, ‘장미가’, ‘생선은’이 주제가 됩니다.

꽃은 장미가 향기가 좋아요

생선은 도미가 맛있다.

Comment  Read more

한국어의 상(aspect)

|

이번 글에서는 한국어의 상(aspect)에 대해 살펴보도록 하겠습니다. 이 글은 고려대 정연주 선생님 강의와 ‘한국어문법총론1(구본관 외 지음, 집문당 펴냄)’을 정리하였음을 먼저 밝힙니다. 그럼 시작하겠습니다.

개념

이란 어떤 사태의 내적 시간 구성을 가리키는 문법 범주입니다. 사태의 시간적 구조나 전개 양상을 바라보는 화자의 관점이 어디에 놓여 있는지를 문법적 수단을 통해 나타난 것입니다. 가령 ‘꽃이 피다’라는 사태도 자세히 들여다보면 그 내적 구조나 전개 양상에 여러 국면이 있습니다. 다음 그림을 보겠습니다.

꽃망울이 터질락 말락하는 상황, 꽃망울이 터지고 있는 상황, 만개(滿開)한 상황을 표현할 때 각각 -려고 하-, -고 있-, -어 있-이 대응하고 있는 점을 볼 수 있습니다. 이처럼 사태의 내적 시간 구성이 어미나 보조용언 같은 문법요소로 실현된 문법범주를 이라고 합니다. 상은 주로 문법요소에 의해 실현된 걸 일컫는 말이지만 한국어에서는 상이 동사 어휘로 나타나기도 합니다. 둘을 구분하기 위해 전자를 문법상(grammatical aspect), 후자를 어휘상(lexical aspect)이라고 합니다.

상 vs 시제

상은 시제(tense)와 구별되는 개념입니다. 시제란 절이나 문장이 나타내는 사태가 발생한 시간적 위치를 나타내는 문법범주입니다. 특정 기준 시점(대개 말하는 시점)과 동시에 일어난 사태를 언급하는 것이라면 현재, 앞선 사태는 과거, 뒤에 일어난 사태는 미래 입니다.

그러나 상은 사태의 시간적 위치와는 크게 관련이 없습니다. 이보다는 그 사태의 내적 시간 구성을 바라보는 화자의 관점이 관건이 됩니다. 다음 예문을 봐도 ‘꽃망울이 터질락 말락하는 상황’을 시제와 관계없이 표현할 수 있습니다.

꽃이 피려고 한다. (꽃망울이 터질락 말락하는 사태가 현재 벌어지고 있음)

꽃이 피려고 했다. (꽃망울이 터질락 말락하는 사태가 과거에 벌어졌음)

꽃이 피려고 할 것이다. (꽃망울이 터질락 말락하는 사태가 미래에 벌어질 것임)

상의 종류

상의 종류를 차례대로 살펴보겠습니다.

완망상(완료상) vs 비완망상(미완료상)

완망상(완료상, perfective aspect)은 사태를 멀리서 하나의 점처럼 바라보는 걸 가리킵니다. 사태를 멀리서 보게 되면 사태의 내부는 안 보이는 대신에 사태의 전모를 시야에 넣을 수 있습니다(완전한 조망). 결과적으로 완료의 의미를 나타내는 셈이죠.

반대로 비완망상(미완료상, inperfective aspect)은 가까이에서 사태의 내적 시간 구조나 전개 양상에 주로 관심을 갖고 바라보는 것입니다(불완전한 조망). 사태를 가까이서 보면 사태의 내부가 잘 보이는 대신 사태의 가장자리(예컨대 시작과 끝), 윤곽 등은 시야에 들어오지 않습니다.

예문을 보겠습니다.

  1. 완망상 : 학교에 영이
  2. 비완망상 : 학교에 가던 영이

예문1의 화자는 영이가 학교에 가는 사태의 끝(=도착)은 알고 있지만, 영이의 등굣길이 어땠는지 시시콜콜한 내용은 모른다는 느낌을 줍니다. 반대로 예문2는 화자가 학교에 가고 있던 영이를 잠깐 보았고 등굣길 모습이 어땠는지 그 사태 내부는 알고 있지만, 실제로 학교에 도착했는지 여부는 모른다는 인상을 풍깁니다.

연속상, 진행상

연속상(continuous), 진행상(progressive)이란 어떤 사태가 특정 시간 구간 내에서 지속되고 있음을 나타냅니다. 진행상은 동적인 사태에 국한된 범주로 연속상이 진행상보다 넓은 개념입니다. 한국어의 경우 연속상에 해당하는 어미가 -고 있-입니다. 다음 예문과 같습니다.

  • 동적인 사태 : 철수가 밥을 먹고 있다.
  • 정적인 사태 : 진이는 철수가 천재라고 생각하고 있다.

결과상, 예정상

결과상(resultative)이란 과거 사태의 결과가 지속됨을, 예정상(prospective)이란 사태가 예정되어 있음을 나타냅니다. 한국어에서는 -려고 하-가 예정상, -어 있-이 결과상에 대응하는 어미입니다. 다음 예문과 같습니다.

  • 예정상 : 꽃이 피려고 한다.
  • 결과상 : 꽃이 피어 있다.

Comment  Read more

Topic Modeling, LDA 구현

|

이번 글에서는 말뭉치로부터 토픽을 추출하는 토픽모델링(Topic Modeling) 기법 가운데 하나인 잠재디리클레할당(Latent Dirichlet Allocation, LDA)을 파이썬 코드로 구현하는 법을 살펴보도록 하겠습니다. 이 글은 ‘밑바닥부터 시작하는 데이터 과학(조엘 그루스 지음, 인사이트 펴냄)’을 정리하였음을 먼저 밝힙니다. LDA 기법 자체에 대한 자세한 내용은 이곳을 참고하시면 좋을 것 같습니다. 그럼 시작하겠습니다.

수식

LDA 모델을 모두 정리하면 $d​$번째 문서 $i​$번째 단어의 토픽 $z_{d,i}​$가 $j​$번째에 할당될 확률은 다음과 같이 쓸 수 있습니다.

[p({ z }_{d, i }=j { z }{ -i },w)=\frac { { n }{ d,k }+{ \alpha }{ j } }{ \sum _{ i=1 }^{ K }{ ({ n }{ d,i }+{ \alpha }{ i }) } } \times \frac { { v }{ k,{ w }{ d,n } }+{ \beta }{ { w }{ d,n } } }{ \sum _{ j=1 }^{ V }{ { v }{ k,j }+{ \beta }_{ j } } }=AB]

위 수식의 표기를 정리한 표는 다음과 같습니다.

표기 내용
$n_{d,k}$ $k$번째 토픽에 할당된 $d$번째 문서의 단어 빈도
$v_{k,w_{d,n}}$ 전체 말뭉치에서 $k$번째 토픽에 할당된 단어 $w_{d,n}$의 빈도
$w_{d,n}$ $d$번째 문서에 $n$번째로 등장한 단어
$α$ 문서의 토픽 분포 생성을 위한 디리클레 분포 파라메터
$β$ 토픽의 단어 분포 생성을 위한 디리클레 분포 파라메터
$K$ 사용자가 지정하는 토픽 수
$V$ 말뭉치에 등장하는 전체 단어 수
$A$ $d$번째 문서가 $k$번째 토픽과 맺고 있는 연관성 정도
$B$ $d$번째 문서의 $n$번째 단어($w_{d,n}$)가 $k$번째 토픽과 맺고 있는 연관성 정도

변수 선언

LDA 학습을 위해서는 변수를 먼저 선언해주어야 합니다. 다음과 같습니다.

from collections import Counter

# 각 토픽이 각 문서에 할당되는 횟수
# Counter로 구성된 리스트
# 각 Counter는 각 문서를 의미
document_topic_counts = [Counter() for _ in documents]

# 각 단어가 각 토픽에 할당되는 횟수
# Counter로 구성된 리스트
# 각 Counter는 각 토픽을 의미
topic_word_counts = [Counter() for _ in range(K)]

# 각 토픽에 할당되는 총 단어수
# 숫자로 구성된 리스트
# 각각의 숫자는 각 토픽을 의미함
topic_counts = [0 for _ in range(K)]

# 각 문서에 포함되는 총 단어수
# 숫자로 구성된 리스트
# 각각의 숫자는 각 문서를 의미함
document_lengths = map(len, documents)

# 단어 종류의 수
distinct_words = set(word for document in documents for word in document)
V = len(distinct_words)

# 총 문서의 수
D = len(documents)

코드 변수명이 조금 생소하실 것 같아서 원 논문의 notation과 비교한 표를 다음과 같이 만들었습니다.

원래 notation code 변수명
$n_{d,k}$ document_topic_counts
$v_{k,w_{d,n}}$ topic_word_counts
$Σ_{i=1}^Kn_{d,i}$ document_lengths
$Σ_{j=1}^Vv_{k,j}$ topic_counts

예컨대 세번째 문서 가운데 토픽 1과 관련있는 단어수는 다음과 같습니다.

document_topic_counts[3][1]

nlp라는 단어가 토픽2와 연관지어 등장한 횟수는 다음과 같습니다.

topic_word_counts[2][‘nlp’]

새로운 topic 계산하기

$d$번째 문서 $i$번째 단어의 토픽 $z_{d,i}$가 $j$번째에 할당될 확률은 $A$와 $B$를 곱해 구합니다. 아래 코드에서 p_topic_given_document가 $A$, p_word_given_topic이 $B$입니다. topic_weight 함수는 이 둘을 곱한 값이라는 걸 알 수 있습니다.

def p_topic_given_document(topic, d, alpha=0.1):
    # 문서 d의 모든 단어 가운데 topic에 속하는
    # 단어의 비율 (alpha를 더해 smoothing)
    return ((document_topic_counts[d][topic] + alpha) /
            (document_lengths[d] + K * alpha))

def p_word_given_topic(word, topic, beta=0.1):
    # topic에 속한 단어 가운데 word의 비율
    # (beta를 더해 smoothing)
    return ((topic_word_counts[topic][word] + beta) /
            (topic_counts[topic] + V * beta))

def topic_weight(d, word, k):
    # 문서와 문서의 단어가 주어지면
    # k번째 토픽의 weight를 반환
    return p_word_given_topic(word, k) * p_topic_given_document(k, d)

$AB$를 구했으니 이를 바탕으로 샘플링을 하여 $z_{d,i}$에 새로운 topic을 할당할 수 있습니다. 그 코드는 다음과 같습니다.

def choose_new_topic(d, word):
    return sample_from([topic_weight(d, word, k) for k in range(K)])

import random
def sample_from(weights):
    # i를 weights[i] / sum(weights)
    # 확률로 반환
    total = sum(weights)
    # 0과 total 사이를 균일하게 선택
    rnd = total * random.random()
    # 아래 식을 만족하는 가장 작은 i를 반환
    # weights[0] + ... + weights[i] >= rnd
    for i, w in enumerate(weights):
        rnd -= w
        if rnd <= 0:
            return i

inference

다음과 같은 데이터가 주어졌다고 칩시다.

documents = [["Hadoop", "Big Data", "HBase", "Java", "Spark", "Storm", "Cassandra"],
    ["NoSQL", "MongoDB", "Cassandra", "HBase", "Postgres"],
    ["Python", "scikit-learn", "scipy", "numpy", "statsmodels", "pandas"],
    ["R", "Python", "statistics", "regression", "probability"],
    ["machine learning", "regression", "decision trees", "libsvm"],
    ["Python", "R", "Java", "C++", "Haskell", "programming languages"],
    ["statistics", "probability", "mathematics", "theory"],
    ["machine learning", "scikit-learn", "Mahout", "neural networks"],
    ["neural networks", "deep learning", "Big Data", "artificial intelligence"],
    ["Hadoop", "Java", "MapReduce", "Big Data"],
    ["statistics", "R", "statsmodels"],
    ["C++", "deep learning", "artificial intelligence", "probability"],
    ["pandas", "R", "Python"],
    ["databases", "HBase", "Postgres", "MySQL", "MongoDB"],
    ["libsvm", "regression", "support vector machines"]]

우선 inference에 필요한 기초 데이터를 만듭니다. 토픽수 $K$ 등 하이퍼파라메터를 정하고, 각 단어를 임의의 토픽에 배정한 뒤 필요한 숫자를 세어봐야 합니다.

random.seed(0)

# topic 수 지정
K=4

# 각 단어를 임의의 토픽에 랜덤 배정
document_topics = [[random.randrange(K) for word in document]
                    for document in documents]

# 위와 같이 랜덤 초기화한 상태에서 
# AB를 구하는 데 필요한 숫자를 세어봄
for d in range(D):
    for word, topic in zip(documents[d], document_topics[d]):
        document_topic_counts[d][topic] += 1
        topic_word_counts[topic][word] += 1
        topic_counts[topic] += 1

우리의 목표는 ‘토픽-단어’와 ‘문서-토픽’에 대한 결합확률분포(unknown)로부터 표본을 얻는 것이므로, 깁스샘플링을 수행하면 됩니다. iteration은 1000으로 설정했습니다.

for iter in range(1000):
    for d in range(D):
        for i, (word, topic) in enumerate(zip(documents[d],
                                              document_topics[d])):
            # 깁스 샘플링 수행을 위해
            # 샘플링 대상 word와 topic을 제외하고 세어봄
            document_topic_counts[d][topic] -= 1
            topic_word_counts[topic][word] -= 1
            topic_counts[topic] -= 1
            document_lengths[d] -= 1

            # 깁스 샘플링 대상 word와 topic을 제외한 
            # 말뭉치 모든 word의 topic 정보를 토대로
            # 샘플링 대상 word의 새로운 topic을 선택
            new_topic = choose_new_topic(d, word)
            document_topics[d][i] = new_topic

            # 샘플링 대상 word의 새로운 topic을 반영해 
            # 말뭉치 정보 업데이트
            document_topic_counts[d][new_topic] += 1
            topic_word_counts[new_topic][word] += 1
            topic_counts[new_topic] += 1
            document_lengths[d] += 1

파일럿 실험 결과

inference 결과 첫번째 문서의 토픽 비중은 다음과 같습니다. 전체 7개 단어 가운데 0번째 토픽과 관련된 단어가 4개 1번째 토픽 단어가 3개입니다. 따라서 이 문서는 첫번째 토픽일 확률이 가장 높네요.

>> document_topic_counts[0]

Counter({0: 4, 2: 3, 1: 0, 3: 0})

첫번째 토픽의 단어 비중은 다음과 같습니다. Java, Big Data, Hadoop, deep learning 등 단어의 빈도(비중)가 높네요. 이 토픽은 대략 ‘Big Data’에 해당하는 주제인 것 같다는 느낌이 듭니다.

>> topic_word_counts[0]

Counter({‘Java’: 3, ‘Big Data’: 3, ‘Hadoop’: 2, ‘deep learning’: 2, ‘artificial intelligence’: 2, ‘C++’: 2, ‘neural networks’: 1, ‘Storm’: 1, ‘programming languages’: 1, ‘MapReduce’: 1, ‘Haskell’: 1, ‘probability’: 0, ‘Mahout’: 0, ‘NoSQL’: 0, ‘MySQL’: 0, ‘regression’: 0, ‘statistics’: 0, ‘Postgres’: 0, ‘Python’: 0, ‘mathematics’: 0, ‘Spark’: 0, ‘numpy’: 0, ‘pandas’: 0, ‘theory’: 0, ‘libsvm’: 0, ‘scipy’: 0, ‘R’: 0, ‘HBase’: 0, ‘decision trees’: 0, ‘MongoDB’: 0, ‘scikit-learn’: 0, ‘machine learning’: 0, ‘databases’: 0, ‘statsmodels’: 0, ‘support vector machines’: 0, ‘Cassandra’: 0})

Comment  Read more

의사결정나무 구현하기

|

이번 포스팅에선 한번에 하나씩의 설명변수를 사용하여 예측 가능한 규칙들의 집합을 생성하는 알고리즘인 의사결정나무(Decision Tree)를 파이썬 코드로 구현하는 법을 다뤄보도록 하겠습니다. 이 글은 ‘밑바닥부터 시작하는 데이터과학(조엘 그루스 지음, 인사이트 펴냄)’을 정리하였음을 먼저 밝힙니다. 의사결정나무에 대한 일반적인 내용은 이곳을 참고하시면 좋을 것 같습니다. 그럼 시작하겠습니다.

분석 대상 데이터

주어진 학습데이터는 다음과 같습니다. 클래스는 True, False 두 개입니다.

inputs = [
    ({'level': 'Senior', 'lang': 'Java', 'tweets': 'no', 'phd': 'no'}, False),
    ({'level': 'Senior', 'lang': 'Java', 'tweets': 'no', 'phd': 'yes'}, False),
    ({'level': 'Mid', 'lang': 'Python', 'tweets': 'no', 'phd': 'no'}, True),
    ({'level': 'Junior', 'lang': 'Python', 'tweets': 'no', 'phd': 'no'}, True),
    ({'level': 'Junior', 'lang': 'R', 'tweets': 'yes', 'phd': 'no'}, True),
    ({'level': 'Junior', 'lang': 'R', 'tweets': 'yes', 'phd': 'yes'}, False),
    ({'level': 'Mid', 'lang': 'R', 'tweets': 'yes', 'phd': 'yes'}, True),
    ({'level': 'Senior', 'lang': 'Python', 'tweets': 'no', 'phd': 'no'}, False),
    ({'level': 'Senior', 'lang': 'R', 'tweets': 'yes', 'phd': 'no'}, True),
    ({'level': 'Junior', 'lang': 'Python', 'tweets': 'yes', 'phd': 'no'}, True),
    ({'level': 'Senior', 'lang': 'Python', 'tweets': 'yes', 'phd': 'yes'}, True),
    ({'level': 'Mid', 'lang': 'Python', 'tweets': 'no', 'phd': 'yes'}, True),
    ({'level': 'Mid', 'lang': 'Java', 'tweets': 'yes', 'phd': 'no'}, True),
    ({'level': 'Junior', 'lang': 'Python', 'tweets': 'no', 'phd': 'yes'}, False)
]

엔트로피 구하기

분기된 영역의 엔트로피를 구하는 코드는 다음과 같습니다.

import math
from collections import Counter, defaultdict

def entropy(class_probabilites):
    # 클래스에 속할 확률을 입력하면 엔트로피 계산
    # 확률이 0인 경우는 제외함
    return sum(-p * math.log(p, 2) for p in class_probabilites if p is not 0)

def class_probabilities(labels):
    # 레이블의 총 개수 계산 : ex) 5
    total_count = len(labels)
    # Counter(labels) = {Class0 : 3, Class1 : 2}
    # class0 prob = 0.6, class1 prob = 0.4 반환
    return [float(count) / float(total_count) for count in Counter(labels).values()]

def data_entropy(labeled_data):
    # 데이터를 받아서 레이블 정보만 뺀 뒤 리스트로 저장
    # ex) labels = [0, 0, 0, 1, 1]
    labels = [label for _, label in labeled_data]
    # 클래스 비율 계산
    probabilities = class_probabilities(labels)
    # 클래스 비율을 토대로 엔트로피 계산
    return entropy(probabilities)

def partition_entropy(subsets):
    # subset은 레이블이 있는 데이터의 list의 list
    # 그에 대한 엔트로피를 계산한 뒤 모든 subset의 엔트로피 합친 값 반환
    total_count = sum(len(subset) for subset in subsets)
    # subset A의 엔트로피는 A 요소별 엔트로피의 합 * A의 영역 비율
    return sum(data_entropy(subset) * len(subset) / total_count for subset in subsets)

def partition_by(inputs, attribute):
    # attribute 기준으로 inputs를 부분 집합으로 분리
    # attribute 변수 내에 3개 값이 있다면 그룹수 = 3
    # ex) level 기준 = Senior, Mid, Junior 3개 그룹
    groups = defaultdict(list)
    for input in inputs:
        # 특정 attribute의 값을 불러옴
        key = input[0][attribute]
        # 이 input을 올바른 list에 추가
        groups[key].append(input)
    return groups

def partition_entropy_by(inputs, attribute):
    # 주어진 파티션에 대응되는 엔트로피를 계산
    partitions = partition_by(inputs, attribute)
    return partition_entropy(partitions.values())

Tree 구축하기

데이터에 재귀적 분기를 실시해 분기된 영역의 순도를 높이는 작업을 반복합니다. 다음과 같습니다.

from functools import partial
def build_tree(inputs, split_candidates=None):
    # 첫 분기라면 입력 데이터의 모든 변수가 분기 후보
    if split_candidates is None:
        # 'lang', 'tweets', 'phd', 'level' 모두 후보
        split_candidates = inputs[0][0].keys()

    # 입력 데이터에서 범주별 개수를 세어 본다
    num_inputs = len(inputs)
    num_class0 = len([label for _, label in inputs if label])
    num_class1 = num_inputs - num_class0

    # class0(true)이 하나도 없으면 False leaf 반환
    if num_class0 == 0: return False
    # class1(false)이 하나도 없으면 Ture leaf 반환
    if num_class1 == 0: return True

    # 파티션 기준으로 사용할 변수가 없다면
    if not split_candidates:
        # 다수결로 결정
        # class0(true)가 많으면 true,
        # class1(false)가 많으면 false 반환
        return num_class0 >= num_class1

    # 아니면 가장 적합한 변수를 기준으로 분기
    best_attribute = min(split_candidates,
                         key=partial(partition_entropy_by, inputs))
    partitions = partition_by(inputs, best_attribute)
    new_candidates = [a for a in split_candidates
                      if a != best_attribute]

    # 재귀적으로 서브트리를 구축
    subtrees = { attribute_value : build_tree(subset, new_candidates)
                 for attribute_value, subset in partitions.iteritems()}
    # 기본값
    subtrees[None] = num_class0 > num_class1 
    return (best_attribute, subtrees)

예측하기

학습된 Tree와 새로운 데이터가 주어졌을 때 해당 데이터가 어떤 범주에 속하는지를 맞추는 예측 코드는 다음과 같습니다.

def classify(tree, input):
    # 주어진 tree를 기준으로 input을 분류
    # 잎 노드이면 값 반환
    if tree in [True, False]:
        return tree

    # 그게 아니면 데이터의 변수로 분기
    # 키로 변수값, 값으로 서브트리를 나타내는 dict 사용
    attribute, subtree_dict = tree

    # 만약 입력된 데이터 변수 가운데 하나가
    # 기존에 관찰되지 않았다면 None
    subtree_key = input.get(attribute)

    # 키에 해당하는 서브트리가 존재하지 않을 때
    if subtree_key not in subtree_dict:
        # None 서브트리를 사용
        subtree_key = None

    # 적절한 서브트리를 선택
    subtree = subtree_dict[subtree_key]
    # 그리고 입력된 데이터를 분류
    return classify(subtree, input)

코드 실행

학습 및 예측 실행 코드는 다음과 같습니다.

tree = build_tree(inputs)
print(classify(tree,
        { "level" : "Junior",
          "lang" : "Java",
          "tweets" : "yes",
          "phd" : "no"} )) # -> True
print(classify(tree,
        { "level" : "Junior",
          "lang" : "Java",
          "tweets" : "yes",
          "phd" : "yes"} )) # -> False

Comment  Read more

빈도 기반의 문장 생성

|

이번 글에서는 말뭉치 빈도 정보를 바탕으로 문장을 생성하는 모델에 대해 살펴보도록 하겠습니다. 이 글은 ‘밑바닥부터 시작하는 데이터 과학(조엘 그루스 지음, 인사이트 펴냄)’을 기본으로 하되 목적에 맞게 파이썬 코드를 적절히 수정했음을 먼저 밝힙니다. 그럼 시작하겠습니다.

데이터 불러오기

이광수 장편소설 ‘무정’을 분석 대상으로 삼았습니다. 소설 원문을 내려받아 어절 단위로 나누어 list 형태로 불러들이는 코드는 다음 한줄이면 됩니다.

document = open('text', 'r', encoding='utf-8').read().split()

바이그램 모델 학습

말뭉치를 두 개 단어씩 슬라이딩해 학습을 먼저 수행해 줍니다. 다음과 같습니다.

from collections import defaultdict
bigram_transitions = defaultdict(list)
for prev, current in zip(document, document[1:]):
    bigram_transitions[prev].append(current)

학습 결과 일부는 다음과 같습니다.

‘말하기’ : [‘미안한’, ‘싫은’, ‘어려운’]

‘나갈’: [‘길을’, ‘방향을’, ‘것’, ‘길이나’]

바이그램 모델 예측

초기 단어를 랜덤으로 선택한 뒤, 이 정보와 학습 결과를 바탕으로 단어 30개를 예측하는 함수는 다음과 같습니다.

import random

def generate_using_bigrams(cut=30):
    idx = 0
    current = random.choice(list(bigram_transitions.keys()))
    result = []
    while True:
        next_word_candidates = bigram_transitions[current]
        current = random.choice(next_word_candidates)
        result.append(current)
        if idx == cut:
            return " ".join(result)
        idx += 1

예측 결과 일부는 다음과 같습니다.

(1) 되었다. 그러나 이 회화를 하며 웃는다. 목사의 뜻을 먼저 더러워졌다’ 하는 것이 아니다. 형식이나 선형에게 대한 처지와 같음이 아닐까. 아까, ‘제가 먹을 것도 생각하였다. 그러나 그것은 내서 무엇 하러 살아왔는고, 하는

(2) 떨어지는 수가 없는 것 모양으로 깨끗한 영혼과 자기의 지금 살아서 우리와 같은 하등 인종으로 알던 것이 매우 양심에 괴롭게 하십니까?” 형식은 모자와 말하는 모양이 생각에만 떠나와도 큰 남자와 같이 자라나던 형식이란

(3) 오랫동안 가물었으므로 대동강물은 꿈에 보던 시와 소설의 기억이 떠나왔던지 모르거니와 적어도 오 리(五里)나 되는 처녀가 처음 보는 눈에는 늘 심로를 하시면서 무엇하러 거기 가서 선 십여 년 지나온 생각이 있다. 계향은

트라이그램 모델 학습

이번엔 단어를 세 개씩 슬라이딩해서 보는 트라이그램 모델을 학습해보겠습니다. 코드는 다음과 같습니다.

trigrams = zip(document, document[1:], document[2:])
trigram_transitions = defaultdict(list)

학습 결과 일부는 다음과 같습니다.

(‘벌떡’, ‘일어나’): [‘모기장을’, ‘방에’, ‘시퍼런’, ‘방’, ‘달려오더니’, ‘문을’]

(‘가만히’, ‘고개를’): [‘숙였다.’, ‘숙이고’, ‘돌린다.’]

트라이그램 모델 예측

트라이그램 모델로 단어 30개를 예측하는 함수는 다음과 같습니다.

def generate_using_trigrams(cut=30):
    idx = 0
    prev, current = random.choice(list(trigram_transitions.keys()))
    result = [current]
    while True:
        next_word_candidates = trigram_transitions[(prev, current)]
        next_word = random.choice(next_word_candidates)
        prev, current = current, next_word
        result.append(current)
        if idx == cut:
            return " ".join(result)
        idx += 1

예측 결과 일부는 다음과 같습니다.

(1) 나는 살려고 난 것 같지를 아니해요. 아버지와 두 오라비를 건져 내려고 기생이 된 것이라. 영채가 평양 감옥에 흙물 옷을 입으신 부친의 얼굴을 대하기는 하였사오나, 무섭게 여윈 그 얼굴을 보았다. 이제 보니 선형이나

(2) 생각에 자기의 지아비는 극히 깨끗하고 점잖은 사람이라야 할 터인데 그러한 소리를 염치없이 하는 형식은 죄인인 듯하다. 더러운 기생에게 하던 버릇을 내게다가 했구나 하고 선형은 정면으로 형식을 본다. 형식은 자기의 변명을 할 기회가

(3) 그는 조선인 교육계에 대하여 항상 불만한 생각을 품는다. 그가 경성교육회라는 것을 설립할 양으로 두어 달을 두고 분주한 것도 이러한 기관을 이용하여 자기의 교육에 대한 이상(理想)을 선전(宣傳)하려 함이었다. 그러나 다른 교사들은 총독부의 고등보통교육령과

Comment  Read more