DNA 포럼 API 서비스 모음 DNA Lens

위키피디아 코퍼스 분석


  • 김경민(CDO본부 데이타마이닝팀), 2007년 1월

웹 2.0을 대표하는 위키피디아는 이미 브리태니커를 넘어선 오픈백과사전으로 유명합니다. 위키피디아에 올라와 있는 데이타들은 웹상의 html과 섞여있는 텍스트와는 달리 사용자들에 의해서 다양한 태그들이 붙여짐으로 구조화되어 있으며 따라서 우리는 이 대용량 코퍼스로부터 유용한 정보들을 추출해 낼 수 있습니다. 본 글에서는 위키피디아 데이타를 가공하는 방법에 대해서 간략하게 설명하도록 하겠습니다.

위키피디아 코퍼스 #

다운로드 #

MySQL에 저장 #

  • gzip -dc dump.xml.gz | php maintenance/importDump.php
: 위와 같이 입력함으로 xml형태의 텍스트를 mysql 데이타베이스로 옮길 수 있다.

  • 위의 작업을 위해서는 www.mediawiki.org에서 위키를 다운받아서 설치를 한 후에 maintenance폴더에 있는 파일로 실행할 수 있다.

코퍼스 통계 정보 #

  • 대상 파일 : enwiki-20060502-pages-meta-current.xml
  • 인코딩 : UTF-8
  • 파일 사이즈 : 약 8.8 기가 (8,844,837,180 Bytes)
  • 표제어 수 : 약 4,103,315개

사용 툴 #

  • Python, PHP, Mysql

데이타 추출 #

추출 대상 #

  • <title> 태그에 의해서 표제어 추출
  • /={2, 6}/ 에 의해서 하위 제목 추출
  • DATA에 의하여 표제어 추출
  • #REDIRECT 관련 데이타 추출

※ 사실 이외에도 볼드체 및 이탤릭체 등 추출가능한 정보들이 더 존재하지만, 데이타 양이 지나치게 많아짐으로 위의 4가지로 한정하였다.

문자열 전처리 #

  • 이렇게 추출된 문자열 가운데 알파벳, 하이픈(-), 어퍼스트로피('), 앰퍼샌드(&)만으로 구성된 것을 제외한 다른 문자열들은 모두 제외한다.
  • Title의 경우 스페이스(공백문자)대신 언더바(_)를 사용했으므로 언더바를 모두 스페이스로 변환해준다.
  • 괄호에 의해서 묶여진 데이타는 원래 추출 문자에는 포함되지 않는 괄호를 가지고 있지만, 위키피디아에서 둥근괄호는 부가적인 설명을 주는 의미가 있으므로 따로 처리해 준다.
    • 예) jaguar (car) : 브라우저상에서는 jaguar라고 보이지만, jaguar의 속성을 car로 둥근괄호를 통해서 보여준다.
  • '|'에 의해서 부가적인 설명이 붙는 문자열의 경우도 특별히 분리해서 따로 각각의 의미를 살려줄 수도 있다. 다만 이렇게 처리할 경우 처리해야할 문자열의 수가 지나치게 많아지게 되는 수가 생기므로 필요에 따라서 추출의 정도를 달리할 수 있다.
    • 예) anarchism|anarchist

추출방법 #

  • 제목과 하위제목은 정규표현식을 가지고 문자열을 치환해가면서 쉽게 추출 가능
  • Internal Link 추출
    • Pushdown automata를 구현해서 추출가능
    • 원래 위키피디아 사이트 소스에서는 ']]'에 의해서 문자열을 분리한 후 '[['을 찾아서 내부 문자열에 링크를 거는 식으로 되어 있으므로 이와 같은 방식으로 처리하는 것이 간단함.

추출한 문자열의 원형 복원 #

  • 추출한 문자열의 원형을 복원하기 위해서 C로 작성된 영어형태소 분석기를 swig를 통해서 파이썬에서 사용가능하도록 포팅하였다. (이 때문에 사실 처음에 PHP가 아닌 파이썬을 사용했지만... 실제 데이타 분석에서는 형태소분석된 문자열을 사용하지 않았다)

  • 파이썬에서 사용하기 위한 Wrapper를 만들기 위하여 ***.i 파일을 만든다. 여기에서는 dema.i를 만들었다.
    • 마지막 getBase 함수는 간단하게 형태소 결과를 받아오는 함수이다.

/* File : dema.i */
%module dema
%{
/* Put headers and other declarations here */
extern int startDaumEMA (const char* pFile, const char* sFile, const char* dFile, const char* iFile);
extern void endDaumEMA();
extern int HANL_AnalyzeEnglish (FINAL_INFO* info, char* instr, int tt);
extern char* getBase (char* in);
%}

extern int startDaumEMA (const char* pFile, const char* sFile, const char* dFile, const char* iFile);
extern void endDaumEMA();
extern int HANL_AnalyzeEnglish (FINAL_INFO* info, char* instr, int tt);
extern char* getBase (char* in);

  1. unix> swig -python dema.i
    • dema_wrap.c 라는 함수를 얻는다.
  2. unix> gcc -c -fpic dema.c dema_wrap.c searchtrie.c affile.c -I/usr/local/include/python2.4
    • searchtrie.c는 사전검색을 위해서 affile.c는 접사 사전검색을 위해서 사용되는 파일들이다.
    • object 파일들을 얻는다.

  3. unix> gcc -shared dema.o dema_wrap.o searchtrie.o affile.o -o _dema.so
    • 다음과 같이 오브젝트 파일들을 다 모아서 _dema.so를 만들어준다. 파일명의 중복을 피하기 위해서 _(under bar)를 붙여준다.

  4. unix> python

  5. >>> import dema
    • dema.i 에서 module이름을 dema라고 해 주었기 때문에 dema를 import한다.

  6. >>> dema.startDaumEMA ("data_prefix", "data_suffix", "entry.db", "infl.db")
    • 사전을 로딩해 준다. 앞에서 부터 접두사, 접밈사, 규칙사전, 불규칙사전 순이다.

  7. >>> dema.getBase("books")
    • 결과값으로 book이 출력된다.

  8. >>> dema.endDaumEMA()
    • 종료된다.

데이타 분석 #

분석 목적 #

  • 어휘간의 관계를 만들어, 특정 문자열에 대해서 관련된 어휘를 관련도에 따라서 보여주는 것

분석 방법 #

  • 하나의 문서에서 제목과 나머지 추출 문자열들의 쌍(pair)을 만들어준다.
  • 하나의 문단에서 추출된 문자열들을 가지고 쌍(pair)을 만들어준다.
  • 이들 정보를 가지고 빈도를 구한다.

관련도 계산식 #

  • 현재의 pair의 빈도 : a
  • 각 pair의 빈도 합 : b
  • 입력 쿼리에 대해 관계를 가지는 문자열 빈도 : c
  • 모든 문자열의 빈도 : d

    ratio = a * log((a/b)/(c/d))

: 빈도가 많은 것이 중요하므로 가중치의 의미로 a를 주었으며, 수식의 의미는 전체 문자열에서 관련 문자열이 가지는 비중과 전체 pair에서 결관 pair가 가지는 비중을 비교함으로 얼마나 상대적으로 현재 관련 문자열이 입력쿼리와 높은 관련도를 가지고 있는지를 보여준다.

SAS Code #

/* for analyzing in remote sever */
rsubmit;

/*
 * Making a SAS sheet for extracted data such as titles and internal links from wikipedia by python program
 */
data wiki_result;
	infile '/home/sas/work_metamoi/text_entry_tbl.txt' dlm='09'x;
	length entry_id 8. entry_sub_id 8. para_id 8. raw_text $50. text_id $16. sub_text $50.;
	input entry_id entry_sub_id para_id raw_text text_id sub_text;
run;


/*
 * sheet split by entry_id which shows one document because of memory management
 */
data text0 text1  text2  text3  text4  text5  text6  text7  text8  text9; 
set wiki_result;
	if mod(entry_id,10) = 0 then output text0;
	if mod(entry_id,10) = 1 then output text1;
	if mod(entry_id,10) = 2 then output text2;
	if mod(entry_id,10) = 3 then output text3;
	if mod(entry_id,10) = 4 then output text4;
	if mod(entry_id,10) = 5 then output text5;
	if mod(entry_id,10) = 6 then output text6;
	if mod(entry_id,10) = 7 then output text7;
	if mod(entry_id,10) = 8 then output text8;
	if mod(entry_id,10) = 9 then output text9;
run;

/*
 * SAS MACRO Declaration
 */
%macro repeat;
%do i = 0 %to 9;
proc sql;

/* making a pair with internal links in a paragraph */
create table text_v1(compress=yes) as
	select a.entry_id, a.para_id, a.sub_text as key1, b.sub_text as key2
		from text&i(where=(text_id ne 'TITLE')) a, text&i(where=(text_id ne 'TITLE')) b
			where a.entry_id = b.entry_id and a.para_id = b.para_id and  a.sub_text ne b.sub_text;

/* making a pair with internal links and title */
create table text_v2(compress=yes) as
	select a.entry_id, a.sub_text as key1, b.sub_text as key2
		from (select * from text&i where text_id = 'TITLE') a, text&i b
			where a.entry_id = b.entry_id and a.sub_text ne b.sub_text;quit;

/* joining the above two results together */
data all;
	set text_v1(keep = key1 key2)  text_v2(keep = key1 key2);
run;

/* computing the frequency of pairs */
proc sql;
create table all_&i(compress=yes) as
	select key1, key2, count(*) as cnt
		from all
			group by 1,2;
quit;
%end;
%mend;

/* run the macro */
%repeat;


/*
 * Joining the above results from MACRO together 
 */
data all;
	set all_0  all_1  all_2  all_3  all_4  all_5  all_6  all_7  all_8  all_9;
run;


/* computing the frequency of pairs */
proc sql;
create table all2(compress=yes) as
	select key1, key2, sum(cnt) as cnt
		from all
			group by 1,2;

/* computing the frequency of terms */
create table zz2(compress=yes) as
	select sub_text, count(*) as cnt
		from wiki_result group by 1;

/* Total of terms */
proc sql;
create table total_term as
	select sum(cnt) as cnt
		from zz2;

/*
 * computing the ratio using the above two tables, zz2, all2
 */
create table all3(compress=yes) as
	select a.*, b.cnt as k1_cnt, c.cnt as k2_cnt ratio as b.cnt * log ((b.cnt*d.cnt)/(k1_cnt*k2_cnt))
		from all2 a, zz2 b, zz2 c, total_term d
			where a.key1 = b.sub_text and a.key2 = c.sub_text;

data a;
	today = today();
	today2 = put(today,yymmddn.);
	call symput('today',today);
	call symput('today2',today2);
run;

/*
 * writing the all3 sheet on the file
 */
data _null_;
	set all3;
	file '/disk/15/metamoi/wiki_final.txt' dlm='09'x;
	put key1 key2 cnt k1_cnt k2_cnt;
run;

/*
 * writing the zz2 sheet on the file
 */
data _null_;
	set zz2;
	file '/disk/15/metamoi/term_tbl.txt' dlm='09'x;
	length sub_text $50. cnt 8.;
	put sub_text cnt;
run;
endrsubmit;


결론 #

  • 이전에는 좀 더 받은 코퍼스를 확보하기 위해서 코퍼스에서 좀더 세밀하게 좀더 많은 정보를 추출하는 것이 목적이었지만, 현재는 정보의 양이 너무 많아져서 사실 정보를 좀 더 많이 추출하는 것이 중요한 것이 아니라 어떻게 좀 더 정제된 정보를 추출하느냐가 데이타 가공에서 가장 중요한 부분이라고 이번 작업을 하면서 생각하게 되었다.
  • 우리가 하고 있는 검색이라는 분야에서도 어떤 면에서 많은 정보가 존재하는데 이들 가운데서 어떻게 데이타를 필터링하고 정제해서 적은 양이지만 정확한 데이타를 보여주느냐가 중요한 부분인 것 같다.
  • 작업 자체는 기술적으로 크게 어려운 부분은 아니지만, 대용량 데이타를 다룰 때에 생기는 시간과 데이타베이스의 성능이 큰 부분이라는 생각이 든다.
  • 위키피디아는 사실 어려 면에서 응용해 볼 수 있는 유용한 데이타라는 생각이 든다. 이를 여러면에서 활용할 수 있을 것 같다.