웹 위주의 글을 다뤄왔었지만 오늘은 확률 모델링에 대한 얘기를 해보려고 한다.
이 분야를 웹만큼은 잘 모르지만 이 모델을 재밌게 만들었어서 글로도 써보려고 한다.
마르코프 체인이란?
먼저 마르코프 체인에 대해 간단히 설명해 보자면,
이 모델은 상태, 전이 확률로 이루어지고 과거의 상태에만 의존하는 특징을 가지고 있다.
상태: 취할 수 있는 특정한 조건이나 상황으로 날씨 예측을 모델링한다고 했을 때 맑음, 흐림, 비 등이 있을 수 있다. 이를 모아둔 집합을 상태 공간이라고 한다.
전이 확률: 상태에서 다른 상태로 전이할 확률로 0과 1 사이의 값을 가지게 된다
예시) 날씨 예측을 모델링한다고 했을 때 전이 확률은 아래와 같은 값을 가질 수 있다.
p(맑음 -> 맑음) = 0.7
p(맑음 -> 흐림) = 0.2
p(맑음 -> 비) = 0.1
(위 전이확률을 해석하면 오늘이 맑음일 때 내일의 날씨가 맑음일 확률은 0.7, 흐림일 확률 0.2, 비가 올 확률 0.1이라는 의미인 것이다.)
문자열 예측
그렇다면 이 모델을 가지고 문자열을 예측하려면 어떻게 해야 할까?
상태의 길이를 1이라고 했을 때 상태는 한 글자인 글자가 될 것이다.
그렇다면 상태 집합은 어떻게 될까? { "안", "녕" }과 같은 글자들로 이루어질 것이다.
그리고 p("안" -> "녕") = 0.3, p("안" -> "전") = 0.1과 같은 전이확률을 가질 수 있을 것 같다.
그렇다면 "안" 뒤에 오는 글자들의 빈도를 활용해 예측해 볼 수 있을 것 같다..!
이걸 구현하기 위해 AIHub에서 제공하는 데이터를 파이썬을 통해 만들었다.
출처(https://aihub.or.kr/aihubdata/data/view.do?currMenu=115&topMenu=100&aihubDataSe=realm&dataSetSn=543)
모델 만들기
import random
import pickle
# 데이터셋
with open('data.txt', 'r') as f:
data = f.readline()
print('데이터셋 크기:', len(data))
def create_markov_model(data, order):
model = {}
for i in range(len(data)-order):
# 현재 문자와 이전 문자열을 키(key)로 사용하여 딕셔너리에 저장
current = data[i:i+order]
next_char = data[i+order]
if current not in model:
model[current] = {}
if next_char not in model[current]:
model[current][next_char] = 0
model[current][next_char] += 1
return model
model_1 = create_markov_model(data, 1)
with open('model/model_1.pkl', 'wb') as f:
pickle.dump(model_1, f)
위 코드를 통해 모델링을 진행했다.
코드를 간단하게 설명하면 이렇다.
1. 이전 문자를 key로 현재 문자를 저장하고 빈도를 계산함
2. 데이터로 주어진 문자열의 길이만큼 이를 반복함
3. 이 딕셔너리를 pkl 형태로 저장함
이 과정을 반복하게 되면 아래와 같은 형태의 자료형이 만들어진다.
"안": {"녕":5, "전":2, "정":3}, ...
위와 같은 자료형을 통해 전이 확률을 얻어낼 수 있다.
p(안 -> 녕): 0.5, p(안 -> 전): 0.2, p(안 -> 정): 0.3
모델을 만들었겠다 한 번 실행을 해보자
실행
import random
import pickle
def predict(model, current):
if current not in model:
# 이전 문자열이 모델에 없을 경우, 임의의 문자열 반환
return random.choice(list(model.keys()))[0]
# 이전 문자열이 모델에 있을 경우, 다음 문자열 예측
next_chars = model[current]
total = sum(next_chars.values())
probabilities = {k: v/total for k, v in next_chars.items()}
return random.choices(list(probabilities.keys()), list(probabilities.values()))[0]
with open('model/model_1.pkl', 'rb') as f:
model_1 = pickle.load(f)
# 테스트
current = input('문자 입력: ')
print('입력된 문자:', current)
print('1개로 예측된 후행 문자:', predict(model_1, current[-1:]))
결과: 안녕하세 -> 요, 고양이 -> 더, 감사합 -> 도
모델 수정
흠 안녕하세요를 만든 것을 빼면 나머지는 아쉽다..
문제점이 뭘까?
상태의 길이를 1로 설정했기 때문에 세 -> 요, 이 -> 더, 합 -> 도 이렇게 예측할 수밖에 없어 이런 결과가 나왔던 것이다.
상태의 길이를 키우면 아래와 같은 자료형이 나와 조금 더 정확한 예측을 할 것으로 예상된다
"안녕": {"하":9, "반":1, "수":1}, ...
import random
import pickle
# 데이터셋
with open('data.txt', 'r') as f:
data = f.readline()
print('데이터셋 크기:', len(data))
def create_markov_model(data, order):
model = {}
for i in range(len(data)-order):
# 현재 문자와 이전 문자열을 키(key)로 사용하여 딕셔너리에 저장
current = data[i:i+order]
next_char = data[i+order]
if current not in model:
model[current] = {}
if next_char not in model[current]:
model[current][next_char] = 0
model[current][next_char] += 1
return model
model_1 = create_markov_model(data, 1)
model_2 = create_markov_model(data, 2)
model_3 = create_markov_model(data, 3)
with open('model/model_1.pkl', 'wb') as f:
pickle.dump(model_1, f)
with open('model/model_2.pkl', 'wb') as f:
pickle.dump(model_2, f)
with open('model/model_3.pkl', 'wb') as f:
pickle.dump(model_3, f)
import random
import pickle
def predict(model, current):
if current not in model:
# 이전 문자열이 모델에 없을 경우, 임의의 문자열 반환
return random.choice(list(model.keys()))[0]
# 이전 문자열이 모델에 있을 경우, 다음 문자열 예측
next_chars = model[current]
total = sum(next_chars.values())
probabilities = {k: v/total for k, v in next_chars.items()}
return random.choices(list(probabilities.keys()), list(probabilities.values()))[0]
with open('model/model_1.pkl', 'rb') as f:
model_1 = pickle.load(f)
with open('model/model_2.pkl', 'rb') as f:
model_2 = pickle.load(f)
with open('model/model_3.pkl', 'rb') as f:
model_3 = pickle.load(f)
# print(model_3)
# 테스트
current = input('문자 입력: ')
print('입력된 문자:', current)
print('1개로 예측된 후행 문자:', predict(model_1, current[-1:]))
print('2개로 예측된 후행 문자:', predict(model_2, current[-2:]))
print('3개로 예측된 후행 문자:', predict(model_3, current[-3:]))
아까 코드에서 조금 수정해 상태의 길이를 2, 3으로 늘려 모델을 만들 수 있게 되었다.
결과는?

"고양이", "감사합"도 상태의 길이가 1일 때 보다 훨씬 그럴듯하게 예측하는 걸 볼 수 있다.
문장 생성?
그렇다면 이 과정을 반복한다면 문장을 만들 수 있을 것 같다는 생각이 든다.
상태의 길이가 3인 model_3을 사용해 문장을 만들어보자

흠.. 뭔가 아쉽다..
원인을 분석해 보았을 때 상태의 길이는 괜찮은 것 같지만 트레이닝 데이터가 충분하지 않아,
중간중간 랜덤의 문자열을 불러오는 경우가 있어 그런 경우에서 끊기는 것 같다..
아마 트레이닝 데이터의 크기를 훨씬 키우면 이런 문제도 사라지고 자동으로 문장을 생성할 수 있을 것 같다.
처음으로 웹이 아닌 다른 분야의 글을 적어보았는데,
자주는 아니지만 종종 이렇게 아예 새로운 분야의 글도 적어보려고 한다 :)