상세 컨텐츠

본문 제목

디지털 역사학 : 김병준 교수님 오픈소스 tokenizer로 분석하는 일제강점기 조선일보 텍스트

디지털 역사학

by 디지털 역사학 2026. 1. 9. 13:40

본문

작년부터 프로그래밍 학원 다니고 있긴 한데,

그냥 컴퓨터 사양만 괜찮으면 그것도 감사하단 마음으로 주로 혼자 공부하고 있다.

 

특히 램이 16기가라서 감사하고

(내 노트북은 몇 기가.....?ㅎ)

그동안 못 했던 거 어찌 다 해 보고 있다.

 

특히 지난 8월에 소개한 김병준 교수님의 오픈소스를 드디어 사용해 볼 수 있게 되었다. 

https://uourstar.tistory.com/10?category=1230943

 

디지털 인문학 공부 : 김병준 교수님과 전봉관 교수님 그리고 이원재 교수님 논문 도전

논문 내용에 대해서는 다음 글에 작성하고,이번 글에서는 왜 두 분 연구에 관심을 가지게 된 건지 등을 얘기하고자 해요. 본론으로,제가 지난 글에 소개한 논문 ' 김병준 and 전봉관. (2023). 민족,

uourstar.tistory.com

 


본론으로,

 

IDE는 VSCODE 사용했고

(학원에서는 이클립스 쓰고 있는데, 진짜 이클립스....

왜 쓰는 건지 모르겠어. 인텔리제이도 아니도, 이클립스......)

 

언어는 파이썬 사용했다.

 

https://github.com/ByungjunKim/ModernKoreanSubword

 

GitHub - ByungjunKim/ModernKoreanSubword: 근대 한국어(대한제국~일제강점기) 서브워드 토크나이저

근대 한국어(대한제국~일제강점기) 서브워드 토크나이저. Contribute to ByungjunKim/ModernKoreanSubword development by creating an account on GitHub.

github.com

 

솔직히 주피터용 언어 몰라서 그냥 내가 아는 것들로 사용했다.ㅎ

예를 들어 주피터 환경에서는  sys.exit() 대신에 return 쓰라던데,

일단 내가 아는 대로만 해도 문제 없이 잘 돌아가더라.

시간이 다소 소요될 수 있습니다. ...라고 적었는데, 생각보다 빨리 됨.

 

그런데 하나 아쉬운 점이 있었다.

교수님께서 올려주신 조선일보 샘플에는 날짜 데이터가 없어서 할 수 있는 게 많지 않았다.

 

마음 같아서는 일제강점기 시차 분석 등을 하고 싶었던 터라 꼼수로 날짜 처리를 해보았는데, 코드가 안 돌아갔다. ㅎ

 

 

 

날짜 데이터가 없으니 Lag 돌려도 나오질 않고,

 

그나마 할 수 있는 건 LDA 같은 것뿐. ㅎ

 

그래도 tokenizer 잘 되어서 한국어와 한자 모두 잘 나오긴 한다.

문제는 불용어 처리 등이 잘 안 된다는 점.............

 

불용어 처리 제대로 안 해서 사람이 1등이 되었네?! 문제는 사람 등 불용어 처리 일일이 다 해줘야 한다는 점.....
사람, 일반 없앴더니 한자 확 늘었음. ㅎㅎㅎ 사람과 일반이 담론일 수도 있으니, 텍스트마이닝에서 함부로 없애선 안 되지만, 일단 결과 달라지니 다행이긴 함. 한자로 된 데이터셋이 정말 작동한다는 게 신기하다. ㅎㅎㅎㅎㅎㅎㅎ

 

 

내가 관심 있는 분야로 사전 설정해서 적용을 하긴 했는데,

사전 설정에 대해 더 공부해야 할 듯하다.

 

(영어나 한국어는 그나마 처리하기 쉽다고 생각했는데, 당장 한국어 처리도 제대로 안 됨.ㅎ)

불용어와 포함어는 정말 간단하게 다음과 같이 코드 써야 돌아가고,

결과물도 매우 간단함.

물론, 저 간단한 걸로도 분석은 가능하다.

 

LDA 머신러닝 로직이 복잡하다면 복잡하겠지만,

내가 임의로 설정한 값을 기반으로 데이터가 나온 거라서 결과 분석이 쉽다.

 

토픽 #1. 일제강점기 사법/치안 및 사건·사고

-핵심 키워드: `裁判(재판)` | `檢事(검사)` | `事件(사건)` | `犯罪(범죄)` | `警察(경찰)` | `被告(피고)`
즉, 이 토픽은 당시 조선총독부 경찰 및 사법 체계 하에서 발생한 형사 사건, 취조, 치안 통제, 그리고 법원 재판 과정을 다루는 기사 묶음.

독립운동가들에 대한 재판이나 사회적 강력 범죄, 사상범 검거 등의 기사가 이 토픽에 강하게 매칭될 테고,

내가 적은 단어 '범죄'가 가중치로 잘 먹힌 듯하다.

토픽 #2. 근대 교육 환경 및 학교 생활

-핵심 키워드:   `學校(학교)` | `學生(학생)` | `敎育(교육)` | `卒業(졸업)` | `兒童(아동)` | `學友(학우)`
즉, 일제강점기 당시 조선의 교육 현실과 학교 현장을 대변하는 기사 묶음.

신학기 학생 모집, 졸업식 풍경, 아동 교육 문제, 혹은 당시 학교 내에서 일어난 동맹휴학(동맹파업) 등 교육계 전반의 소식을 담고 있을 듯하다.  '교육' 키워드도 잘 적용된 듯함.



토픽 #3: 국제 정세 및 외교·전쟁
-핵심 키워드:  `政府(정부)` | `國際(국제)` | `英國(영국)` | `交涉(교섭)` | `條約(조약)` | `委員(위원)`
즉, 1920~40년대의 세계 정세와 열강들의 외교전을 다룬 기사 묶음인 듯함.  영국의 대외 정책, 국제 연맹 및 각국 정부 간의 조약 체결, 영토 교섭 등 주로 국제 뉴스나 군사·정치적 긴장 관계를 보도한 내용들이 이 토픽으로 분류된 듯.

그러면 '테러'는 어디로 간 거지??? 국제 정세가 '테러'야?

(참고로 테러 관련 얘기는 후속 포스팅에 작성했다.)

https://uourstar.tistory.com/29

 

디지털 역사학 : tokenizer를 잘 쓰려면 뭘 더 공부해야 할까

"공유 및 검토 목적이 달성된 듯하여, 자세한 내용은 비공개 전환했습니다.현재 관련 내용은 연구계획서와 Git 저장소에서 별도 관리하고 있으며공개 글에서는 문제의식 중심으로만 소개하게 되

uourstar.tistory.com

 

=== 6. 신문 기사 토픽별 핵심 키워드 결과 ===
▶ 토픽 #1: 裁判 | 檢事 | 事件 | 犯罪 | 警察 | 被告 | 地方 | 執行
▶ 토픽 #2: 學校 | 學生 | 敎育 | 卒業 | 兒童 | 學友 | 經營 | 學年
▶ 토픽 #3: 政府 | 國際 | 英國 | 交涉 | 條約 | 委員 | 代表 | 關係
------------------------------------------------------------
=== 7. 토픽별 핵심 키워드 중요도 그래프 시각화 및 저장 ===
📊 노이즈 단어가 제거된 그래프가 './OUTPUT\FIGURE1.png'에 안전하게 저장되었습니다!
------------------------------------------------------------
=== 8. 전체 기사 내 토픽별 점유율(비중) 시각화 및 저장 ===
📊 토픽별 문서 점유율 그래프가 './OUTPUT\FIGURE2.png'에 저장되었습니다!
------------------------------------------------------------
모든 머신러닝 분석 및 시각화(FIGURE1, FIGURE2) 저장 프로세스가 성공적으로 완료되었습니다!


 

 

 

오늘의 결론.

블랙박스 기법 & 기계 학습이라 세부 로직은 모르겠는데,

코드는 어찌 돌아가고 결과가 나와서 기쁘다.

 

물론, 결과를 믿을 순 없지.

 

LDA(Latent Dirichlet Allocation, 잠재 디리클레 할당) 경우

사람이 미리 "1번은 정치, 2번은 교육으로 나눠라" 하고 기준을 정해주는 분류 기법이 아닐 뿐더러

알고리즘이 스스로 기사 데이터를 읽고 통계적 확률에 따라 나누는 비지도 학습인 터라

내가 넣은 필수 용어가 어떻게 학습된 건지 모르겠네????????

 

LDA는 주로 다음의 세 가지를 기반으로,

통계적으로 가장 뭉쳐있는 단어 조합 기준으로 나뉘어야 할 텐데,

정말 저랬을까?

 

LDA 세 가지 말뭉치 기준

1. 단어의 동시 출현 빈도 (Co-occurrence):
    알고리즘은 하나의 기사 안에서 어떤 단어들이 자주 함께 등장하는지 파악
  예> 어떤 기사들에 '학교', '학생', '선생'이라는 단어가 계속 같이 붙어 다닌다면,

        LDA는 이 단어들을 하나의 묶음(토픽)으로 인지.


2. 기사별 토픽의 비율 (Document-Topic Distribution):
    LDA는 "모든 기사는 하나의 토픽으로만 이루어져 있지 않고, 여러 토픽이 섞여 있다"고 가정

  예> 어떤 기사는 [교육 70% + 범죄 30%]의 비율을 가짐)
      이 기사별 토픽 혼합 비율을 가장 잘 설명할 수 있는 최적의 토픽 조합(여기서는 3개)을 수학적으로 찾아내는 것.


3. 단어의 토픽 기여도 (Word-Topic Distribution):
  각 토픽 안에서 해당 토픽을 대표하는 단어들의 가중치(확률값)를 계산.

 예> '사람' 같은 단어는 기여도 높아짐.

 

 

추신.  코드 단순 버전.

import os
import pandas as pd
import matplotlib.pyplot as plt
from kiwipiepy import Kiwi
from kiwipiepy.sw_tokenizer import SwTokenizer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# 폰트 설정 (Mac/Win 공통 대응)
plt.rcParams['font.family'] = 'Malgun Gothic' if os.name == 'nt' else 'AppleGothic'
plt.rcParams['axes.unicode_minus'] = False

# 1. 모델 로드
kiwi = Kiwi()
tokenizer = SwTokenizer('./ModernKoreanSubword/tokenizer/241112_vo64000_tokenizer.json', kiwi=kiwi)

# 2. 데이터 불러오기
df = pd.read_csv('chosun_1920_1945_sample.xlsx - Sheet1.csv')
print(f"데이터 로드 완료: {len(df)}건")

# 3. 전처리 (함수 하나로 압축)
def clean(text):
    if not isinstance(text, str): return ""
    tokens = [t.lstrip('##') for t in [tokenizer.id2vocab[i] for i in tokenizer.encode(text) if i < len(tokenizer.id2vocab)]]
    
    # 필수어/불용어 처리
    must = ['범죄', '테러', '교육']
    stop = ['사람', '일반', '시내', '자긔', '다시', '지금', '다음']
    
    res = []
    for t in tokens:
        word = t.split('/')[0] if '/' in t else t
        if word in must: res.append(word)
        elif word in stop or len(word) < 2 or word.isdigit(): continue
        elif '/' in t and t.split('/')[1] in ['NNG', 'NNP']: res.append(word)
        elif '/' not in t: res.append(word)
    return " ".join(res)

df['clean'] = df['text'].apply(clean)

# 4. 벡터라이징 및 LDA 학습
cv = CountVectorizer(max_features=1000)
tf = cv.fit_transform(df['clean'])
lda = LatentDirichletAllocation(n_components=3, random_state=42)
lda.fit(tf)

# 5. 결과 확인 (키워드 출력)
feat = cv.get_feature_names_out()
for i, comp in enumerate(lda.components_):
    print(f"토픽 {i+1}: {' '.join([feat[x] for x in comp.argsort()[-8:]])}")

# 6. 시각화 (한 번에)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, comp in enumerate(lda.components_):
    top_idx = comp.argsort()[-10:]
    axes[i].barh([feat[x] for x in top_idx], comp[top_idx])
    axes[i].set_title(f'Topic {i+1}')
plt.tight_layout()
plt.show()

# 파이차트 (분석의 비중 확인)
plt.pie(lda.transform(tf).mean(axis=0), labels=[f'T{i+1}' for i in range(3)], autopct='%.1f%%')
plt.show()

관련글 더보기