import numpy as np
import matplotlib.pyplot as plt
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Title")
plt.plot(x, y)
위치 변경하기
x축, y축, 제목은 모두 위치를 변경할 수 있습니다.
위치는 두 가지 방법으로 옮길 수 있습니다.
1. pad를 사용하여 간격을 조절
import numpy as np
import matplotlib.pyplot as plt
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.xlabel("X-axis", labelpad=40)
plt.ylabel("Y-axis", labelpad= 30)
plt.title("Title", pad=30)
plt.plot(x, y)
각 축과 제목의 간격이 멀어지신게 보이시나요?
pad 내 숫자가 커질수록 그래프와 축/제목 사이의 간격을 멀게 설정할 수 있습니다.
ㄴ
2. 좌표를 설정하여 위치를 조절
제목은 좌표를 설정해서 위치를 조절할 수 있습니다!
축의 경우에도 동일하게 좌표 설정을 할 수 있는데, 좌표대로 잘 움직이지 않아 거의 사용하지 않습니다ㅠㅠ
import numpy as np
import matplotlib.pyplot as plt
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(6, 4))
# 글씨 크기 조절 가능
plt.xlabel("X-axis", fontsize=14)
plt.ylabel("Y-axis", fontsize=14)
ax.set_title("Title", fontsize=14, x=0.8, y=1.05)
plt.plot(x, y)
제목의 위치가 변경되신게 보이시나요?
x는 좌우의 위치를, y는 상하의 위치를 변경할 수 있습니다!
범례 설정하기
범례란?
범례는 지도나 차트 등에서 참고하라는 뜻으로 나타낸 정보입니다.
파이썬 시각화에서는 보통 각 그래프가 어떤 것을 나타내는지 표기할 때 많이 사용합니다!
아래 그래프처럼 노란색과 연두색이 각각 어떤 그래프를 나타내는지 아래쪽에 표기된 것이 범례입니다.
범례 생성하기
보통 범례는 자동으로 생성되는 경우가 많은데, 그래프를 각각 그릴 경우에는 범례가 생성되지 않습니다.
이 때 직접 범례를 설정하는 것도 가능합니다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.cos(x)
y1 = np.sin(x)
line1, = plt.plot(x, y, color='lightskyblue')
line2, = plt.plot(x, y1, color='lightcoral')
# 범례 직접 설정
plt.legend(handles=[line1, line2], labels=["Cos(x)", "Sin(x)"])
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Legend Example")
plt.show()
plt.legend(handles=[line1, line2], labels=["Cos(x)", "Sin(x)"]) 여기서 loc = 옵션을 추가하게 되면 범례의 위치를 어느 정도 조정할 수 있습니다!
예를 들어 upper right 옵션으로 하게 되면, 오른쪽 위에 범례가 생성되는데요, 옵션을 정하지 않으면 가장 적당한 위치에 알아서 생성이 됩니다.
ㄷ
위치 변경하기
위의 사진처럼 범례가 자동으로 생성될 때 그래프를 가리는 경우를 자주 접하실 수 있는데요!
이 때 범례 위치를 변경하는 코드는 알아두시면 유용합니다:)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.cos(x)
y1 = np.sin(x)
line1, = plt.plot(x, y, color='lightskyblue')
line2, = plt.plot(x, y1, color='lightcoral')
# 범례 직접 설정
plt.legend(handles=[line1, line2], labels=["Cos(x)", "Sin(x)"], loc='lower right', bbox_to_anchor=(0.81, 0.03))
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Legend Example")
plt.show()
범례의 위치가 변경된 게 보이시나요?
지금은 예쁘게 옮긴 건 아니지만, 범례를 자유롭게 움직일 수 있는 것은 굉장히 편리하니 잘 사용해주세요!
먼저 loc 옵션을 조정하여 큰 틀의 위치를 정해주시고, bbox_to_anchor 내 좌표로 세세한 위치를 조정해주시면 됩니다:)
한글 설정하기
마지막으로 각 축, 제목, 범례를 한글로 정하는 방법에 대해서 알려드리겠습니다!
보통 한글로 설정을 하게 되면 아래 사진처럼 한글이 깨져서 나오기 때문에 한글 설정이 먼저 필요합니다.
한글 설정을 위해서는 먼저 한글 폰트를 찾아야 합니다.
C:\Windows\Fonts 해당 경로로 가시면, 컴퓨터에 설치되어 있는 폰트를 보실 수 있습니다!
이제 저희가 사용하고 싶은 폰트를 고르면 되는데, 아쉽게도 모든 폰트를 지원하지는 않습니다ㅠㅠ
파이썬은 바탕, 굴림, 궁서체 중 골라서 사용하시는게 안전합니다:) (그래도 이것저것 해보시는 걸 추천 드려요)
위의 글씨체 중 하나를 골라 마우스 오른쪽 클릭→속성→이름 복사를 하시면 되는데, 이름은 .ttc 앞까지만 복사해주세요!
※ 만약에 속성이 나타나지 않는다면, 폰트를 더블 클릭해서 들어가신 다음 진행하시면 됩니다.
간혹 HY시리즈는 이름 그대로를 사용하셔야 되는 경우도 있습니다.
예를 들면 HYPost의 경우 HYPost-Medium, HY고딕의 경우 HYGothic-Medium을 사용합니다.
ㄹ
이제 아래 코드를 실행하시게 되면 한글 지원이 가능합니다.
plt.rcParams['font.family'] = 'HYPost-Medium'
이제 한글로 잘 보이는 걸 알 수 있습니다!!
하지만 한글로 변경할 때는 종종 숫자의 마이너스가 깨지는 경우가 있어요ㅠㅠ
해당 경우는 마이너스가 지원되는 한글을 써야하는데, 저는 보통 굴림을 사용합니다.
plt.rcParams['font.family'] = 'gulim'
이제 한글과 마이너스가 모두 잘 보이는 것을 확인할 수 있습니다!
여기까지 축, 제목, 범례 활용을 정리해보았습니다!
이것저것 쓰다보니 꽤 길어졌는데요, 시각화는 예쁘면 예쁠수록 도움이 되기 때문에 세세한 부분이라도 잘 활용하시면 좋을 것 같습니다:)
특히 한글 설정 같은 경우, 글씨체가 이쁘면 보기도 좋으니 여러 폰트로 한 번 사용해보시길 추천드려요ദ്ദി・ᴗ・)✧
안녕하세요! 오늘은 파이썬으로 하는 시각화 활용 Line plot에 대해 포스팅 하려고 합니다.
Line plot이란?
시간이나 연속적인 값을 나타낼 때 사용되는 그래프로, 일반적인 선 그래프 입니다.
보통 x축에는 연속적인 변수를 y축에는 수치형 데이터를 배치해서 사용하는 경우가 일반적입니다.
저는 보통 식을 그릴 때는 matplotlib, 데이터 프레임이 있는 경우에는 seaborn, matplotlib 두 개를 함께 사용해서 line plot을 그립니다.
matplotlib로 단일 그래프 그리기
우선 먼저 matplotlib를 사용해서 간단한 그래프를 그려보겠습니다.
아래처럼 숫자를 직접 입력하거나, 특정 식이 존재한다면 matplotlib만 사용해서 그리는 것이 간단합니다!
import matplotlib.pyplot as plt
# 왼쪽이 x 값, 오른쪽이 y 값
plt.plot([1, 2, 3, 4], [2, 3, 5, 10])
plt.show()
import numpy as np
import matplotlib.pyplot as plt
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.sin(x)
plt.plot(x, y)
plt.show()
seaborn을 함께 사용하여 단일 그래프 그리기
seaborn은 데이터 프레임과 호환성이 좋기 때문에 보통 데이터 프레임으로 사용합니다.
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 데이터 생성
x = np.linspace(0, 10, 100)
y = np.cos(x)
df = pd.DataFrame({"X": x, "Y": y})
sns.lineplot(x="X", y="Y", data=df)
그래프 커스텀 하기
사실 그래프를 그리는 것은 정말 간단합니다!
하지만 그래프를 단순히 그리는 것과 이를 커스텀해서 사용하는 것은 정말 큰 차이가 있습니다.
Q 값이 제대로 업데이트가 될 때까지충분한 탐험을 진행하지 못했을 경우, 제대로 된 행동을 추출할 수 없다는 것입니다.
예를 들어,초반에는 Q 값이 모두 0이기 때문에(0, 0)에서왼쪽으로 가는 행동만 선택하기 때문에 게임을 진행할 수가 없습니다.
저희는 이 문제를 해결하기 위해,ϵ-greedy방법을 사용할 수 있습니다.
ϵ-greedy란?
탐험과 이용의 균형을 맞추기 위한 행동 선택 방법으로, 아래 공식을 따릅니다.
여기서 ϵ은 0과 1 사이의 값으로ϵ 확률만큼은랜덤하게 행동을 하게 하여탐험을 진행하도록 하고,(1-ϵ)확률 만큼Q 값이 가장 높은 행동을 선택하도록 합니다.
해당 ϵ을 초반에 높게 설정하고 점차 ϵ을 줄임으로써,초반에는 랜덤 행동을 통한 탐험을 하게 하고 점차 Q 값을 이용하도록 행동을 선택할 수 있습니다.
ϵ-greedy를 활용하여 행동을 선택하는 코드는 아래와 같습니다.
# 초기 값은 보통 1로 설정
epsilon = 1.0
train = True
# ϵ-greedy를 활용한 행동 선택
def select_action(state) :
# 훈련을 할 경우에는 ϵ-greedy 방법을 사용
# 테스트를 진행할 때는 온전히 Q 값만 사용
# np.random.rand()를 넣어, 후반에도 종종 탐험을 할 수 있도록 함
if np.random.rand() < epsilon and train :
action = np.random.choice([0, 1, 2, 3])
else :
action = np.argmax(Q[state])
return action
선택한 행동을 SARSA 공식을 사용해서 Q 값 업데이트
해당 부분은 위에서선택한 행동을 환경에서 실행해보고, 그결과 값을 SARSA 공식에 맞게 Q 값을 업데이트하는 것입니다.
코드로 들어가기 전에 먼저Q 테이블을 업데이트하는 공식을 먼저 살펴보겠습니다.
해당 수식은 Q(s, a)를 업데이트 하는데, 특정 학습률 α에 있어(1- α)만큼 현재의 Q 값과α만큼의 (보상값 r + 할인율 γ * 다음 state와 action의 Q값 Q(s', a'))를 반영한다는 의미입니다.
Q-learning을 배우신 분들은 아시겠지만, Q-learning에서의 maxQ값이 특정 행동 a'의 Q 값인 Q(s', a')로 바뀐 것을 알 수 있습니다.
학습률 α
값이 높을수록다음 행동 값 즉,새로운 정보를 더 많이 반영한다는 것이고,낮을수록현재의 Q 값 즉,기존의 경험을 더 많이 유지한다는 의미입니다.
할인율 γ
미래 보상의 중요도를 나타내는 지표로, 보통은 미래의 보상에 너무 의존하지 않도록1보다 약간 작은 수로 지정하는 것이 보통입니다.
Q(s', a')
실제로 선택한 다음 행동 a'에 대한 Q 값으로, 위의 행동 선택을 기반하여 다음 state에서 실제 action을 고른 값입니다.
이렇게 실제 행동을 기반으로 Q 값을 업데이트 하기 때문에, 안정적이지만 그만큼 느릴 수 있습니다.
하지만, frozen lake는 공간이 작은 문제라서 Q-learning과 크게 결과 차이는 없으실 거예요:)
해당 공식을 바탕으로 SARSA를 진행하는 코드는 아래와 같습니다.
# 학습을 진행할 때는 render 모드 비활성화
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery)
env.reset()
# 환경의 행동 범위 : 여기서는 상, 하, 좌, 우 총 4개
action_size = env.action_space.n
# defaultdict은 키가 없을 때 자동으로 기본값을 생성하기 때문에 강화 학습에서 많이 사용
Q = defaultdict(lambda: np.zeros(action_size))
alpha = 0.1
gamma = 0.99
# 총 학습을 진행할 에피소드 수
max_episode = 10000
def learn() :
reward_list = []
for i in range(1, max_episode+1) :
# 100번째 마다 학습이 진행되고 있음을 출력
if i % 100 == 0 :
# 해당 에피소드까지 진행된 모든 보상의 평균을 구함
avg_reward = sum(reward_list)/100
print("\rEpisode {}/{} || average reward {}".format(i, max_episode, avg_reward), end="")
reward_list = []
# 에피소드를 처음 시작할 때 reset
state, _ = env.reset()
done = False
all_reward = 0
# 에피소드가 종료될 때까지 반복
while not done :
# Q 테이블을 바탕으로 action을 고르는 함수
action = select_action(state)
# state, reward, done 외 사용하지 않기 때문에 _ 처리
new_state, reward, done, _, _ = env.step(action)
next_action = select_action(new_state)
# SARSA
Q[state][action] = (1-alpha)*Q[state][action] + alpha*(reward + gamma*Q[new_state][next_action])
all_reward += reward
state = new_state
# 50번째 에피소드 마다 ϵ 값을 줄여줌
if i % 50 == 0 :
epsilon *= 0.99
reward_list.append(all_reward)
위의 두 가지 과정을 합치면 SARSA로 frozen lake를 풀 수 있는 코드가 완성됩니다!
학습 후테스트를 진행하고 싶으신 경우에는render를 킨 환경을 다시 세팅해서 해주시면 됩니다.
전체 코드
행동 선택, 학습, 테스트 과정을 모두 포함한 전체 코드는 아래와 같습니다.
import gymnasium as gym
from collections import defaultdict
import numpy as np
# 미끄러짐 옵션 True/False 선택 가능
is_slippery = True
# 8x8 중에 선택 가능
map_size = '4x4'
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery)
env.reset()
action_size = env.action_space.n
Q = defaultdict(lambda: np.zeros(action_size))
alpha = 0.1
gamma = 0.99
epsilon = 1.0
train = True
max_episode = 100000
def select_action(state) :
if np.random.rand() < epsilon and train :
action = np.random.choice([0, 1, 2, 3])
else :
action = np.argmax(Q[state])
return action
def learn() :
global epsilon
reward_list = []
for i in range(1, max_episode+1) :
# 100번째 마다 학습이 진행되고 있음을 출력
if i % 100 == 0 :
# 해당 에피소드까지 진행된 모든 보상의 평균을 구함
avg_reward = sum(reward_list)/100
print("\rEpisode {}/{} || average reward {}".format(i, max_episode, avg_reward), end="")
reward_list = []
state, _ = env.reset()
done = False
all_reward = 0
while not done :
action = select_action(state)
new_state, reward, done, _, _ = env.step(action)
next_action = select_action(new_state)
# SARSA
Q[state][action] = (1-alpha)*Q[state][action] + alpha*(reward + gamma*Q[new_state][next_action])
all_reward += reward
state = new_state
if i % 50 == 0 :
epsilon *= 0.99
reward_list.append(all_reward)
# 학습한 Q를 바탕으로 frozen lake 테스트
def testing_after_learning():
# render를 켜야 제대로 학습이 되었는지 확인할 수 있음
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery, render_mode='human')
total_test_episode = 10
rewards = []
for episode in range(total_test_episode):
state, _ = env.reset()
episode_reward = 0
while True:
action = select_action(state)
new_state, reward, done, _, _ = env.step(action)
episode_reward += reward
if done:
rewards.append(episode_reward)
break
state = new_state
print("")
print("avg: " + str(sum(rewards) / total_test_episode))
if __name__ == "__main__" :
learn()
testing_after_learning()
테스트를 진행하면서is_slippery 옵션을 껐을 경우에는1.0 보상을 받으면 성공이고,is_slippery 옵션을 켰을 경우에는70% 이상 1.0 보상을 받으면 성공이라고 보실 수 있습니다.
추가로 is_slippery 옵션을 켰을 경우에는 학습을 많이 진행해야 어느 정도 수렴하시는 걸 보실 수 있습니다!
아무래도 model-free로 진행을 하니까 많이 느리더라구요ㅠㅠ
model-based
model-free가 아닌 어느 정도 model-based로 빠르게 학습을 하고 싶으신 경우 아래 상황을 고려할 수 있습니다.
행동 한 번을 진행할 때마다reward에 - 진행 → RL이 최단 경로로 진행하려는 경향을 학습할 수 있음
구멍에 빠졌을 경우,reward에 크게 - 진행 → 구멍에 빠지지 않는 쪽으로 빠르게 학습할 수 있음
도착했을 경우,reward를 크게 추가 → 도착 지점에 확실히 도착하기 위해 큰 reward를 지급
그 외에도 벽에 부딪히거나 하는 등 맵을 알고 있기 때문에 환경에 맞게reward를 추가로 주거나 마이너스를 진행하여,model-based 모델을 만들 수도 있습니다.
그래도 강화 학습을 제대로 알기 위해서는 model-free로 진행해보는 것을 추천드립니다!
Q-learning으로 frozen lake를 풀 때, 크게 두 가지를 생각하시면 됩니다.
Q 값을 바탕으로 행동을 선택
선택한 행동을 Q-learning 공식을 사용해서 Q 값 업데이트
위의 두 가지를 하나씩 자세히 살펴보겠습니다.
Q 값을 바탕으로 행동을 선택
해당 부분은 현재까지 업데이트 된 Q 값을 바탕으로 행동을 선택하는 것입니다.
기본적으로는 Q 값이 가장 높은 행동을 선택하면 됩니다.
하지만 여기에는 한 가지 문제점이 있습니다.
Q 값이 제대로 업데이트가 될 때까지 충분한 탐험을 진행하지 못했을 경우, 제대로 된 행동을 추출할 수 없다는 것입니다.
예를 들어, 초반에는 Q 값이 모두 0이기 때문에 (0, 0)에서 왼쪽으로 가는 행동만 선택하기 때문에 게임을 진행할 수가 없습니다.
저희는 이 문제를 해결하기 위해, ϵ-greedy 방법을 사용할 수 있습니다.
ϵ-greedy란?
탐험과 이용의 균형을 맞추기 위한 행동 선택 방법으로, 아래 공식을 따릅니다.
여기서 ϵ은 0과 1 사이의 값으로 ϵ 확률 만큼은 랜덤하게 행동을 하게 하여 탐험을 진행하도록 하고, (1-ϵ) 확률 만큼 Q 값이 가장 높은 행동을 선택하도록 합니다.
해당 ϵ을 초반에 높게 설정하고 점차 ϵ을 줄임으로써, 초반에는 랜덤 행동을 통한 탐험을 하게 하고 점차 Q 값을 이용하도록 행동을 선택할 수 있습니다.
ϵ-greedy를 활용하여 행동을 선택하는 코드는 아래와 같습니다.
# 초기 값은 보통 1로 설정
epsilon = 1.0
train = True
# ϵ-greedy를 활용한 행동 선택
def select_action(state) :
# 훈련을 할 경우에는 ϵ-greedy 방법을 사용
# 테스트를 진행할 때는 온전히 Q 값만 사용
# np.random.rand()를 넣어, 후반에도 종종 탐험을 할 수 있도록 함
if np.random.rand() < epsilon and train :
action = np.random.choice([0, 1, 2, 3])
else :
action = np.argmax(Q[state])
return action
선택한 행동을 Q-learning 공식을 사용해서 Q 값 업데이트
해당 부분은 위에서 선택한 행동을 환경에서 실행해보고, 그 결과 값을 Q-learning 공식에 맞게 Q 값을 업데이트 하는 것입니다.
코드로 들어가기 전에 먼저 Q 테이블을 업데이트하는 공식을 먼저 살펴보겠습니다.
해당 수식은 Q(s, a)를 업데이트 하는데, 특정 학습률 α에 있어 (1- α)만큼 현재의 Q 값과 α만큼의 (보상값 r + 할인율 γ * 다음 state의 가장 높은 Q값 maxQ(s', a'))를 반영한다는 의미입니다.
학습률 α
값이 높을수록 다음 행동 값 즉, 새로운 정보를 더 많이 반영한다는 것이고, 낮을수록 현재의 Q 값 즉, 기존의 경험을 더 많이 유지한다는 의미입니다.
할인율 γ
미래 보상의 중요도를 나타내는 지표로, 보통은 미래의 보상에 너무 의존하지 않도록 1보다 약간 작은 수로 지정하는 것이 보통입니다.
maxQ(s', a')
다음 상태인 s'에서 가능한 모든 행동 중 가장 높은 Q 값을 의미하며, s'은 현재 state에서 위에서 고른 행동을 실행한 결과 값이라고 보시면 됩니다.
이제 해당 공식을 바탕으로 Q-learning을 하는 코드는 아래와 같습니다.
# 학습을 진행할 때는 render 모드 비활성화
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery)
env.reset()
# 환경의 행동 범위 : 여기서는 상, 하, 좌, 우 총 4개
action_size = env.action_space.n
# defaultdict은 키가 없을 때 자동으로 기본값을 생성하기 때문에 강화 학습에서 많이 사용
Q = defaultdict(lambda: np.zeros(action_size))
alpha = 0.1
gamma = 0.99
# 총 학습을 진행할 에피소드 수
max_episode = 10000
def learn() :
reward_list = []
for i in range(1, max_episode+1) :
# 100번째 마다 학습이 진행되고 있음을 출력
if i % 100 == 0 :
# 해당 에피소드까지 진행된 모든 보상의 평균을 구함
avg_reward = sum(reward_list)/100
print("\rEpisode {}/{} || average reward {}".format(i, max_episode, avg_reward), end="")
reward_list = []
# 에피소드를 처음 시작할 때 reset
state, _ = env.reset()
done = False
all_reward = 0
# 에피소드가 종료될 때까지 반복
while not done :
# Q 테이블을 바탕으로 action을 고르는 함수
action = select_action(state)
# state, reward, done 외 사용하지 않기 때문에 _ 처리
new_state, reward, done, _, _ = env.step(action)
# Q-learning 공식
Q[state][action] = (1-alpha)*Q[state][action] + alpha*(reward + gamma*np.max(Q[new_state]))
all_reward += reward
state = new_state
# 50번째 에피소드 마다 ϵ 값을 줄여줌
if i % 50 == 0 :
epsilon *= 0.99
reward_list.append(all_reward)
위의 두 가지 과정을 합치면 Q-learing으로 frozen lake를 풀 수 있는 코드가 완성됩니다!
학습 후 테스트를 진행하고 싶으신 경우에는 render를 킨 환경을 다시 세팅해서 해주시면 됩니다.
전체 코드
행동 선택, 학습, 테스트 과정을 모두 포함한 전체 코드는 아래와 같습니다.
import gymnasium as gym
from collections import defaultdict
import numpy as np
# 미끄러짐 옵션 True/False 선택 가능
is_slippery = True
# 8x8 중에 선택 가능
map_size = '4x4'
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery)
env.reset()
action_size = env.action_space.n
Q = defaultdict(lambda: np.zeros(action_size))
alpha = 0.1
gamma = 0.99
epsilon = 1.0
train = True
max_episode = 100000
def select_action(state) :
if np.random.rand() < epsilon and train :
action = np.random.choice([0, 1, 2, 3])
else :
action = np.argmax(Q[state])
return action
def learn() :
global epsilon
reward_list = []
for i in range(1, max_episode+1) :
# 100번째 마다 학습이 진행되고 있음을 출력
if i % 100 == 0 :
# 해당 에피소드까지 진행된 모든 보상의 평균을 구함
avg_reward = sum(reward_list)/100
print("\rEpisode {}/{} || average reward {}".format(i, max_episode, avg_reward), end="")
reward_list = []
state, _ = env.reset()
done = False
all_reward = 0
while not done :
action = select_action(state)
new_state, reward, done, _, _ = env.step(action)
Q[state][action] = (1-alpha)*Q[state][action] + alpha*(reward + gamma*np.max(Q[new_state]))
all_reward += reward
state = new_state
if i % 50 == 0 :
epsilon *= 0.99
reward_list.append(all_reward)
# 학습한 Q를 바탕으로 frozen lake 테스트
def testing_after_learning():
# render를 켜야 제대로 학습이 되었는지 확인할 수 있음
env = gym.make('FrozenLake-v1', desc=None, map_name=map_size, is_slippery=is_slippery, render_mode='human')
total_test_episode = 10
rewards = []
for episode in range(total_test_episode):
state, _ = env.reset()
episode_reward = 0
while True:
action = select_action(state)
new_state, reward, done, _, _ = env.step(action)
episode_reward += reward
if done:
rewards.append(episode_reward)
break
state = new_state
print("")
print("avg: " + str(sum(rewards) / total_test_episode))
if __name__ == "__main__" :
learn()
testing_after_learning()
테스트를 진행하면서 is_slippery 옵션을 껐을 경우에는 1.0 보상을 받으면 성공이고, is_slippery 옵션을 켰을 경우에는 70% 이상 1.0 보상을 받으면 성공이라고 보실 수 있습니다.
추가로 is_slippery 옵션을 켰을 경우에는 학습을 많이 진행해야 어느 정도 수렴하시는 걸 보실 수 있습니다!
아무래도 model-free로 진행을 하니까 많이 느리더라구요ㅠㅠ
model-based
model-free가 아닌 어느 정도 model-based로 빠르게 학습을 하고 싶으신 경우 아래 상황을 고려할 수 있습니다.
행동 한 번을 진행할 때마다 reward에 - 진행 → RL이 최단 경로로 진행하려는 경향을 학습할 수 있음
구멍에 빠졌을 경우, reward에 크게 - 진행 → 구멍에 빠지지 않는 쪽으로 빠르게 학습할 수 있음
도착했을 경우, reward를 크게 추가 → 도착 지점에 확실히 도착하기 위해 큰 reward를 지급
그 외에도 벽에 부딪히거나 하는 등 맵을 알고 있기 때문에 환경에 맞게 reward를 추가로 주거나 마이너스를 진행하여, model-based 모델을 만들 수도 있습니다.
그래도 강화 학습을 제대로 알기 위해서는 model-free로 진행해보는 것을 추천드립니다!
Q-learning이랑 비슷한 SARSA로 frozen lake 문제를 푸는 방법은 아래 링크를 참고해주세요.
안녕하세요! 오늘은 원신 나무위키에 플레이어블 캐릭터와 성유물 카테고리의 글을 크롤링하는 코드에 대해 포스팅 진행하겠습니다.
해당 포스팅에서는 전체 코드와 결과물 이미지만 첨부합니다.
크롤링의 자세한 과정은 추후에 포스팅 진행하도록 하겠습니다.
해당 크롤링은 원신 각 캐릭터의 성유물 추천 옵션과 세트를 빠르게 파악하기 위한 데이터 수집을 목적으로 하고 있습니다!
나무위키에서 수집할 정보는 아래와 같습니다.
※ 사진 속 정보는 나히다를 예시로 한 것입니다.
1. 캐릭터의 이름, 속성, 무기
2. 권장 성유물 옵션
3. 추천 성유물 세트 및 설명
4. 성유물 이름, 세트 효과, 획득처
크롤링 진행 방식
크롤링은 총 3개의 코드로 진행을 합니다.
첫 번째 코드
원신 캐릭터의 상세 정보가 담긴 링크를 전부 긁어옵니다.
캐릭터의 이름과 링크만 저장하여 하나의 엑셀 파일로 저장합니다.
두 번째 코드
첫 번째 코드에서 저장한 엑셀 파일에서 각 캐릭터의 상세 정보 링크를 가져옵니다.
캐릭터의 속성, 무기, 권장 성유물 옵션, 추천 성유물 세트 및 상세 설명의 내용을 가지고 옵니다.
캐릭터의 이름, 속성, 무기 권장 성유물 옵션을 저장하여 하나의 엑셀 파일로 저장합니다.
캐릭터의 이름, 추천 성유물 세트, 상세 설명을 저장하여 하나의 엑셀 파일로 저장합니다.
엑셀 파일을 두 개로 나눈 이유는 이후에 원신 캐릭터 성유물을 조회하는 엑셀 파일을 쉽게 만들기 위해서 입니다!
세번째 코드
성유물 세트 이름, 2세트, 4세트, 획득처의 내용을 가지고 옵니다.
획득처에서 비경의 이름을 분리합니다.
성유물 세트 이름, 2세트, 4세트, 획득처, 비경의 이름을 저장하여 하나의 엑셀 파일로 저장합니다.
비경 이름을 분리한 이유는 이후 원신 캐릭터 성유물을 조회하는 엑셀 파일을 쉽게 만들기 위해서 입니다!
[실제 코드 및 결과물]
첫 번째 코드
import pandas as pd
from selenium import webdriver
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as E
from openpyxl import *
# 원신/캐릭터 나무위키 링크
link = 'https://namu.wiki/w/%EC%9B%90%EC%8B%A0/%EC%BA%90%EB%A6%AD%ED%84%B0'
# 나무위키는 BeautifulSoup이 먹히지 않기 때문에 동적 크롤링으로 진행
driver = webdriver.Chrome('chromedriver.exe')
driver.get(link)
time.sleep(3)
# '원소별' 버튼 클릭
button = list(driver.find_elements(By.CLASS_NAME, "_3xTXXXtF"))
button[1].click()
time.sleep(3)
Character = pd.DataFrame({'캐릭터 이름' : [], '링크' : []})
character_info = driver.find_elements(By.CLASS_NAME, "s3zppxXT")
for i in range(len(character_info)) :
character = character_info[i]
# 캐릭터 이름만 담기 위해서 데이터를 정제하는 부분
# 캐릭터의 소개가 끝나는 부분
if character.text == '취소선' :
break
# 주인공 캐릭터인 아이테르와 루미네는 데이터 수집에서 제외
# 캐릭터 이름이 아닌데, 들어온 정보는 모두 제외
if character.text == '' or '원신' in character.text or '아이테르' in character.text or character.text in ['불', '물', '바람', '번개', '풀', '얼음', '바위'] :
pass
else :
# 캐릭터의 이름
name = character.text
# 캐릭터의 이름이 길 경우, 엔터로 구분이 되어있기 때문에 이를 띄어쓰기로 변경
if '\n' in name :
name = name.replace('\n', ' ')
Char = [name, str(character.get_attribute('href'))]
Character.loc[i] = Char
with pd.ExcelWriter('genshin_link.xlsx') as writer :
Character.to_excel(writer, sheet_name='링크', index=False)
첫 번째 코드 결과물
두 번째 코드
import pandas as pd
from selenium import webdriver
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as E
from openpyxl import *
# 캐릭터의 이름과 상세정보 링크가 담긴 엑셀 파일
link = pd.read_excel('genshin_link.xlsx')
Main_link = list(link['링크'])
Character = list(link['캐릭터 이름'])
Information = pd.DataFrame({'캐릭터 이름' : [], '무기' : [], '시간의 모래' : [], '공간의 성배' : [], '이성의 왕관' : [], '부옵션' : []})
Relic_Information = pd.DataFrame({'캐릭터 이름' : [], '성유물' : [], '평가': []})
driver = webdriver.Chrome('chromedriver.exe')
# 엑셀 전체 인덱스를 의미
# 저장할 엑셀이 두 개이기 때문에 인덱스도 두 개가 필요
total_index = 0
relic_index = 0
# 특정 캐릭터의 상세정보에서 오류가 발생할 경우을 대비
try :
for k in range(len(Main_link)) :
driver.get(Main_link[k])
time.sleep(3)
# 캐릭터의 무기 수집 과정
attack = driver.find_elements(By.CLASS_NAME, 'cIflhYhI')
for i in range(len(attack)) :
if '무기' == attack[i].text :
attack_index = i+1
break
weapon = attack[attack_index].text
# 캐릭터의 성유물 수집 과정
info = driver.find_elements(By.CLASS_NAME, 'D7SMSdcV')
for i in range(len(info)) :
# 권장 성유물 옵션을 파악하기 위해 위치를 저장
if '권장 성유물' in info[i].text :
index = i
break
# 권성유물의 정보가 담긴 공간
sung = info[index]
# 권장 성유물 옵션 수집 과정
options = sung.find_elements(By.CLASS_NAME, 'cIflhYhI')
Option = [Character[k], weapon]
for j in range(len(options)) :
# 권장 성유물 옵션에서 필요한 정보가 들어있는 부분
if j in [4, 5, 6, 8] :
option = options[j].text
# 옵션이 여러 개일 경우, 줄바꿈으로 구분하기 때문에 이를 / 구분으로 변경
if '\n' in option :
option = option.replace('\n', ' / ')
Option.append(option)
# 권장 성유물의 옵션만 담는 부분
Information.loc[total_index] = Option
total_index = total_index+1
# 추천 성유물 세트 및 상세 설명 수집 과정
sets = sung.find_elements(By.CLASS_NAME, 'W078FM6Z')
# 성유물 세트는 캐릭터마다 여러 개 존재하기 때문에 이를 구분하기 위한 부분
character_number = 1
for j in range(len(sets)) :
# li로 구분되어 있는데, 그 안에 div가 같이 들어가 있기 때문에 문제가 발생한다.
relic_info = list(sets[j].text.split('\n'))
for m in range(len(relic_info)) :
# 실제 정보가 들어가 있는 부분
if m%2 == 1:
one_set = relic_info[m-1]
set_info =relic_info[m]
character_name = Character[k]+'%d' %(character_number)
Option = [character_name, one_set, set_info]
Relic_Information.loc[relic_index] = Option
character_number = character_number+1
relic_index = relic_index+1
except Exception as e :
print(e)
print(Main_link[k])
with pd.ExcelWriter('genshin.xlsx') as writer :
Information.to_excel(writer, sheet_name='성유물 옵션', index=False)
with pd.ExcelWriter('genshin_set_relic.xlsx') as writer :
Relic_Information.to_excel(writer, sheet_name='성유물', index=False)
두 번째 코드 결과물
세 번째 코드
import pandas as pd
from selenium import webdriver
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as E
from openpyxl import *
# 원신/성유물 나무위키 링크
link = 'https://namu.wiki/w/%EC%9B%90%EC%8B%A0/%EC%84%B1%EC%9C%A0%EB%AC%BC'
driver = webdriver.Chrome('chromedriver.exe')
driver.get(link)
time.sleep(3)
Relic = pd.DataFrame({'성유물 세트' : [], '2세트' : [], '4세트' : [], '획득처' : [], '비경' : []})
total_index = 0
# 성유물 세트 효과 수집 과정
info = driver.find_elements(By.CLASS_NAME, 'TiHaw-AK._6803dcde6a09ae387f9994555e73dfd7')
for i in range(len(info)) :
# 첫 번째 성유물이 검투사의 피날레이기 때문에 해당 부분이 기준
# 여기서부터 끝까지가 성유물에 대한 정보가 존재
if '검투사' in info[i].text :
index_start = i
break
# 전체적으로 3단위로 원하는 정보가 있음
for i in range(index_start, len(info), 3) :
relic_info = info[i].text.split('\n')
# 1세트 효과가 있는 4성 성유물은 생략한다.
if '모시는 자' in relic_info[0] :
continue
# 획득처에서 비경을 구분하는 과정
if '비경' in relic_info[7] :
place_index_start = relic_info[7].index(':')
place = relic_info[7][place_index_start+2:]
if ',' in place :
place_end_index = place.index(',')
place = place[:place_end_index]
else :
place = ''
relic = [relic_info[0], relic_info[3], relic_info[5], relic_info[7], place]
Relic.loc[total_index] = relic
total_index = total_index+1
with pd.ExcelWriter('genshin_relic.xlsx') as writer :
Relic.to_excel(writer, sheet_name='성유물', index=False)
세 번째 코드 결과물
해당 데이터를 활용하여 원신 캐릭터의 성유물을 엑셀에서 쉽게 조회하는 포스팅은 아래에서 확인해주세요!
import pandas as pd
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import time
from selenium.webdriver.common.by import By
from openpyxl import *
# 2024.01.25 부터 변경된 네이버 기사를 새로 크롤링하기 위해 만든 코드
link = 'https://news.naver.com/breakingnews/section/105/229?date='
# 스크랩 하고 싶은 날짜를 년도월일 나열해준다.
# 날짜를 쉽게 바꾸기 위해 date를 따로 선언해준다.
date = '20250107'
# 메인 링크는 링크에 날짜가 붙은 구조이기 때문에 이렇게 작성해준다.
main_link = link + date
Main_link = pd.DataFrame({'number' : [], 'title' : [], 'link' : []})
# Selenium 4 버전 이상 부터는 해당 방법으로 사용해야 driver 인식이 된다.
service = Service('chromedriver.exe')
driver = webdriver.Chrome(service=service)
driver.get(main_link)
time.sleep(3)
# 기사 더보기 버튼
more_button = driver.find_element(By.CLASS_NAME, 'section_more_inner._CONTENT_LIST_LOAD_MORE_BUTTON')
# 기사 더보기가 몇 개가 있을지 모르기 때문에 오류가 날 때까지 누르는 것으로 한다.
# 여기서 발생하는 오류란 버튼을 찾을 수 없다 즉, 버튼이 없을 때 발생하는 오류이다.
while True :
try :
more_button.click()
time.sleep(3)
except :
break
articles = driver.find_elements(By.CLASS_NAME, 'sa_text_title._NLOG_IMPRESSION')
for i in range(len(articles)) :
title = articles[i].text.strip()
link = articles[i].get_attribute('href')
li = [i+1, title, link]
Main_link.loc[i] = li
excel_name = 'news_' + date + '.xlsx'
with pd.ExcelWriter(excel_name) as writer :
Main_link.to_excel(writer, sheet_name='링크', index=False)
[두 번째 코드]
전체 코드
from bs4 import BeautifulSoup
import requests
import pandas as pd
from openpyxl import *
import time
import urllib
# 첫 번째 코드에서 지정한 뉴스의 링크들이 담긴 파일
link = pd.read_excel('news_20231222.xlsx')
# 엑셀 파일이 헷갈리지 않게 최종 결과파일에도 날짜를 넣어줌
excel_name = 'news_detail_20231222.xlsx'
Main_link = list(link['link'])
# number: 기사의 수, title: 기사의 제목, information: 본문 내용, link: 기사의 링크
Information = pd.DataFrame({'number' : [], 'title' : [], 'information' : [], 'link' : []})
# 본문 내용만 추가하면 되기 때문에 데이터 프레임에 미리 나머지 내용을 담아줌
Information['number'] = link['number']
Information['title'] = link['title']
Information['link'] = link['link']
information = []
for main_link in Main_link :
# 기사가 전체적으로 2개의 구조를 가지고 있음 (게임/리뷰 카테고리에 한하여)
# 하나의 구조를 기준으로 삼고, 해당 부분에서 오류가 발생하면 다음 구조의 기사로 판단
try :
response = requests.get(main_link, headers={'User-Agent':'Moailla/5.0'})
if response.status_code == 200 :
html = response.content
soup = BeautifulSoup(html, 'html.parser')
# 기사의 본문 내용만 담고 있는 부분
info = soup.find('div', {'id' : 'newsct_article'}).text.strip()
# 기사 내용 데이터 분석을 위해서 줄바꿈을 띄어쓰기로 변경
info = info.replace('\n', '')
information.append(info)
except :
# 다른 구조의 기사 크롤링 코드
# 여기서 오류가 나는 경우는 게임/리뷰 기사가 아닌 다른 카테고리의 기사로 판단
try :
response = requests.get(main_link, headers={'User-Agent':'Moailla/5.0'})
if response.status_code == 200 :
html = response.content
soup = BeautifulSoup(html, 'html.parser')
# 기사의 본문 내용을 담고 있는 부분
info = soup.find('div', {'id' : 'newsEndContents'}).text.strip()
info = info.replace('\n', '')
# 해당 구조의 기사는 기자의 정보가 본문과 무조건 같이 존재
# 기자의 정보 부분은 필요가 없기 때문에 기자 정보의 기준점이 되는 부분을 찾음
# 기자의 정보 기준이 기사제공이라는 단어이기 때문에 그 이후는 삭제
end = info.index('기사제공')
info = info[:end]
information.append(info)
# 다른 카테고리의 기사가 들어올 경우에는 정보를 담지 않는 것으로 함
except Exception as e :
info = ''
information.append(info)
# 오류가 발생하는 이유와 발생하는 링크를 출력하여 오류를 확인하는 장치
#print(e)
#print(main_link)
Information['information'] = information
with pd.ExcelWriter(excel_name) as writer :
Information.to_excel(writer, sheet_name='결과값', index=False)
저는 2024년 01월 17일 네이버 게임/리뷰 카테고리의 기사 크롤링 데이터를 불러와 정제를 진행하도록 하겠습니다.
# 파일 이름만 적을 때는 파일이 실행 파일과 같은 곳에 저장되어 있어야 한다.
result = pd.read_excel('파일 이름')
# 기사의 제목 데이터
Title = list(result['title'])
# 기사의 내용 데이터
Information = list(result['information'])
# 기사의 제목과 내용을 하나의 리스트에 담았다.
Total = []
for i in range(len(result)) :
Total.append(Title[i]+' '+Information[i])
※ Total 데이터의 양이 너무 많아서 데이터 확인은 진행하지 않겠습니다.
명사로 데이터 분류하기
토픽을 제대로 분류하기 위해서는 데이터를 의미 있는 데이터만 남기는 것이 중요합니다.
그 중 가장 빠른 방법이 명사인 데이터만 남기는 것인데요.
명사로 분류를 하지 않을 경우, '있다', '있는' 과 같이 보기만 하면 이해하지 못하는 단어들이 높은 비중을 차지하는 경우가 많기 때문에 제대로 데이터 분석이 되지 않는 경우가 많이 발생합니다.
# 형태소 분석기로 Komoran을 사용
komoran = Komoran()
# Total 데이터를 명사로 분류한 후에 띄어쓰기로 붙여넣기 진행
# 줄바꿈으로 진행하도 상관없으나, 줄바꿈으로 진행 시, 이후 띄어쓰기 대신 모두 줄바꿈으로 변경해야한다.
total_nouns = [' '.join(komoran.nouns(doc)) for doc in Total]
추가 전처리 진행하기
total_nouns 데이터는 이제 명사로만 이루어진 데이터입니다.
그대로 토픽 모델링을 진행해도 되지만, 생각보다 의미 없는 데이터가 많이 존재하기 때문에 추가적으로 데이터 전처리를 진행해주는 것이 좋습니다.
예를 들면 '것', '이', '등' 과 같은 단어를 삭제하기 위해서 두 글자 명사만 넣어준다거나, 특정 카테고리의 뉴스이기 때문에 자주 등장하는 명사는 제거한다거나, 기업의 이름들이 명사로 이상하게 분류되어 있는 부분을 원래 기업 이름으로 변경을 해준다거나 하는 방법으로 데이터 전처리를 진행해주시면 됩니다.
제가 진행한 전처리 코드는 아래와 같습니다.
# 추가 데이터 전처리 과정
for i in range(len(total_nouns)) :
# 자주 등장하는 단어들을 꾸준히 붙여준다. (기업 이름 등)
# total_nouns[i]]가 하나의 문자열이기 때문에 reaplace를 통해 변경한다.
total_nouns[i] = total_nouns[i].replace('위 메이드', '위메이드')
total_nouns[i] = total_nouns[i].replace('위 믹스', '위믹스')
total_nouns[i] = total_nouns[i].replace('컴투스 홀', '컴투스홀딩스')
total_nouns[i] = total_nouns[i].replace('개발 사', '개발사')
total_nouns[i] = total_nouns[i].replace('펄 어비스', '펄어비스')
total_nouns[i] = total_nouns[i].replace('콜 라보', '콜라보')
total_nouns[i] = total_nouns[i].replace('카 테 고리', '카테고리')
total_nouns[i] = total_nouns[i].replace('확률 형', '확률형')
total_nouns[i] = total_nouns[i].replace('역대 급', '역대급')
total_nouns[i] = total_nouns[i].replace('마비 노기', '마비노기')
total_nouns[i] = total_nouns[i].replace('게임 위', '게임위')
total_nouns[i] = total_nouns[i].replace('컬 래 버 레이 션', '콜라보레이션')
total_nouns[i] = total_nouns[i].replace('콜 라보 레이 션', '콜라보레이션')
total_nouns[i] = total_nouns[i].replace('빅 게임', '빅게임')
total_nouns[i] = total_nouns[i].replace('엔 씨', '엔씨')
total_nouns[i] = total_nouns[i].replace('스타트 업', '스타트업')
total_nouns[i] = total_nouns[i].replace('디바 이스', '디바이스')
total_nouns[i] = total_nouns[i].replace('선택 지', '선택지')
total_nouns[i] = total_nouns[i].replace('치지 직', '치지직')
total_nouns[i] = total_nouns[i].replace('어 플리 케이 션', '어플리케이션')
total_nouns[i] = total_nouns[i].replace('게임 쇼', '게임쇼')
total_nouns[i] = total_nouns[i].replace('아스 달', '아스달')
total_nouns[i] = total_nouns[i].replace('김실 장', '김실장')
total_nouns[i] = total_nouns[i].replace('행 안부', '행안부')
# 게임 뉴스이기 때문에 게임과 관련된 부분, 뉴스와 관련된 부분은 제거한다.
total_nouns[i] = total_nouns[i].replace('게임', '')
total_nouns[i] = total_nouns[i].replace('기자', '')
total_nouns[i] = total_nouns[i].replace('기사', '')
total_nouns[i] = total_nouns[i].replace('진행', '')
total_nouns[i] = total_nouns[i].replace('이용자', '')
total_nouns[i] = total_nouns[i].replace('플레이', '')
total_nouns[i] = total_nouns[i].replace('이번', '')
# 매일매일 기사에서 반복되는 단어들을 삭제한다.
# 의미가 없는 단어들은 아니지만, 지속적으로 나오면서 의미를 부여하기 어려운 단어가 되었다.
total_nouns[i] = total_nouns[i].replace('지난해', '')
total_nouns[i] = total_nouns[i].replace('전년', '')
total_nouns[i] = total_nouns[i].replace('콘텐츠', '')
total_nouns[i] = total_nouns[i].replace('출시', '')
total_nouns[i] = total_nouns[i].replace('서비스', '')
total_nouns[i] = total_nouns[i].replace('모바일', '')
total_nouns[i] = total_nouns[i].replace('제공', '')
total_nouns[i] = total_nouns[i].replace('예정', '')
# 단어가 두 글자 이상인 것만 토픽 모델링을 진행할 데이터에 넣어준다.
a = total_nouns[i].split(' ')
data = ''
for j in a :
if len(j) >= 2 :
# 동일한 이유로 띄어쓰기로 붙여 넣는다.
# 마찬가지로 줄바꿈으로 진행해도 된다.
data = data+' '+j
total_nouns[i] = data
저는 total_nouns의 일부를 확인하고 진행을 하고 있기 때문에 여러분들은 여러분들의 데이터에 맞게 전처리를 진행하시면 됩니다!
LDA 모델에 학습하기 알맞게 데이터 변형하기
데이터 전처리가 끝난 후에는 LDA 모델에 학습하기 알맞게 데이터를 변형해야 합니다.
데이터를 변형하는 코드는 아래와 같습니다.
# CountVectorizer 객체 생성
# CountVectorizer는 문서에서 단어의 빈도수를 계산하는 도구이다.
CV_vectorizer = CountVectorizer()
# total_nouns에 있는 단어의 빈도수를 행렬로 변경한다.
X = CV_vectorizer.fit_transform(total_nouns)
LDA 모델 생성 및 데이터 학습
이제 데이터가 완성되었으니, LDA 모델을 만들어 데이터를 학습시키도록 하겠습니다!!
LDA 모델을 만드는 코드는 아래와 같습니다.
# 토픽의 개수를 지정한다.
num_topics = 6
# LDA 모델을 생성한다.
# 동일한 결과물을 얻기 위해서 random_state(난수)를 42로 고정한다.
lda = LatentDirichletAllocation(n_components=num_topics, random_state=42)
# 위에서 만든 데이터 X를 LDA 모델에 학습을 시킨다.
# 이제 lda는 데이터 X가 6개의 토픽으로 분류된 정보가 담겨있다.
lda.fit(X)
각 토픽 내 주요 키워드 찾기
토픽으로 분류를 완료하였으니, 각 토픽이 어떤 키워드를 가지고 있는지 확인해보도록 하겠습니다.
저는 각 토픽마다 7개의 키워드를 추출해서 데이터 프레임을 새로 만들었습니다!
키워드의 수는 원하는대로 지정하시면 됩니다.
키워드를 추출하는 코드는 아래와 같습니다.
# CountVectorizer를 통해 추출된 단어의 목록을 얻는다.
# 단어의 목록은 array로 저장되어 있다.
CV_feature_names = CV_vectorizer.get_feature_names_out()
# 각 토픽의 키워드를 담을 리스트
# 여기에 초기화를 진행해주지 않으면, 다른 날짜의 기사를 진행할 때 진행이 잘 되지 않을 수 있다.
topic_keywords = []
# 토픽 수를 구분하는 변수
topic_index = 1
# 키워드 수를 구분하는 변수
# 키워드 수를 변경하고 싶다면, 숫자를 원하는 키워드 수로 변경하면 된다.
num_word = 7
# lda.components_가 이중 array로 되어 있기 때문에 데이터를 쉽게 다루기 위해수 enumerate로 데이터를 가져온다.
for topic_idx, topic in enumerate(lda.components_):
# topic에는 단어의 빈도 확률이 들어있기 때문에 가장 높은 빈도 확률 7개의 인덱스를 추출한다.
top_keywords_idx = topic.argsort()[::-1][:num_word]
# 단어 목록에서 빈도 확률과 동일한 인덱스를 가진 단어를 추출한다.
top_keywords = [CV_feature_names[i] for i in top_keywords_idx]
# 토픽을 구분하는 값을 맨 앞에 삽입해준다.
top_keywords.insert(0, 'Topic %d' %(topic_index))
topic_index = topic_index+1
topic_keywords.append(top_keywords)
# 추출한 7개의 키워드를 데이터 프레임으로 변경한다.
df_topic_keywords = pd.DataFrame(topic_keywords, columns=["Topic"]+ [f"Keyword {i+1}" for i in range(num_word)])
이렇게 만들어진 df_topic_keywords의 결과물은 아래와 같습니다!
저는 6개의 토픽과 7개의 키워드로 진행을 했기 때문에 이런 결과가 나왔습니다.
실제 토픽의 수와 비슷할수록 정확하게 토픽을 구분하지만, 실제 토픽의 수를 알 수 없으니 다양하게 해보시길 바랍니다.
전체코드
import pandas as pd
from konlpy.tag import *
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
result = pd.read_excel('파일 이름')
Title = list(result['title'])
Information = list(result['information'])
Total = []
for i in range(len(result)) :
Total.append(Title[i]+' '+Information[i])
komoran = Komoran()
total_nouns = [' '.join(komoran.nouns(doc)) for doc in Total]
# 전처리 과정
for i in range(len(total_nouns)) :
total_nouns[i] = total_nouns[i].replace('위 메이드', '위메이드')
total_nouns[i] = total_nouns[i].replace('위 믹스', '위믹스')
total_nouns[i] = total_nouns[i].replace('컴투스 홀', '컴투스홀딩스')
total_nouns[i] = total_nouns[i].replace('개발 사', '개발사')
total_nouns[i] = total_nouns[i].replace('펄 어비스', '펄어비스')
total_nouns[i] = total_nouns[i].replace('콜 라보', '콜라보')
total_nouns[i] = total_nouns[i].replace('카 테 고리', '카테고리')
total_nouns[i] = total_nouns[i].replace('확률 형', '확률형')
total_nouns[i] = total_nouns[i].replace('역대 급', '역대급')
total_nouns[i] = total_nouns[i].replace('마비 노기', '마비노기')
total_nouns[i] = total_nouns[i].replace('게임 위', '게임위')
total_nouns[i] = total_nouns[i].replace('컬 래 버 레이 션', '콜라보레이션')
total_nouns[i] = total_nouns[i].replace('콜 라보 레이 션', '콜라보레이션')
total_nouns[i] = total_nouns[i].replace('빅 게임', '빅게임')
total_nouns[i] = total_nouns[i].replace('엔 씨', '엔씨')
total_nouns[i] = total_nouns[i].replace('스타트 업', '스타트업')
total_nouns[i] = total_nouns[i].replace('디바 이스', '디바이스')
total_nouns[i] = total_nouns[i].replace('선택 지', '선택지')
total_nouns[i] = total_nouns[i].replace('치지 직', '치지직')
total_nouns[i] = total_nouns[i].replace('어 플리 케이 션', '어플리케이션')
total_nouns[i] = total_nouns[i].replace('게임 쇼', '게임쇼')
total_nouns[i] = total_nouns[i].replace('아스 달', '아스달')
total_nouns[i] = total_nouns[i].replace('김실 장', '김실장')
total_nouns[i] = total_nouns[i].replace('행 안부', '행안부')
total_nouns[i] = total_nouns[i].replace('게임', '')
total_nouns[i] = total_nouns[i].replace('기자', '')
total_nouns[i] = total_nouns[i].replace('기사', '')
total_nouns[i] = total_nouns[i].replace('진행', '')
total_nouns[i] = total_nouns[i].replace('이용자', '')
total_nouns[i] = total_nouns[i].replace('플레이', '')
total_nouns[i] = total_nouns[i].replace('이번', '')
total_nouns[i] = total_nouns[i].replace('지난해', '')
total_nouns[i] = total_nouns[i].replace('전년', '')
total_nouns[i] = total_nouns[i].replace('콘텐츠', '')
total_nouns[i] = total_nouns[i].replace('출시', '')
total_nouns[i] = total_nouns[i].replace('서비스', '')
total_nouns[i] = total_nouns[i].replace('모바일', '')
total_nouns[i] = total_nouns[i].replace('제공', '')
total_nouns[i] = total_nouns[i].replace('예정', '')
a = total_nouns[i].split(' ')
data = ''
for j in a :
if len(j) >= 2 :
data = data+' '+j
total_nouns[i] = data
CV_vectorizer = CountVectorizer()
X = CV_vectorizer.fit_transform(total_nouns)
num_topics = 6
lda = LatentDirichletAllocation(n_components=num_topics, random_state=42)
lda.fit(X)
CV_feature_names = CV_vectorizer.get_feature_names_out()
topic_keywords = []
topic_index = 1
num_word = 7
for topic_idx, topic in enumerate(lda.components_):
top_keywords_idx = topic.argsort()[::-1][:num_word]
top_keywords = [CV_feature_names[i] for i in top_keywords_idx]
top_keywords.insert(0, 'Topic %d' %(topic_index))
topic_index = topic_index+1
topic_keywords.append(top_keywords)
df_topic_keywords = pd.DataFrame(topic_keywords, columns=["Topic"]+ [f"Keyword {i+1}" for i in range(num_word)])
활용하기
제가 개인적으로 토픽 모델링과 다른 시각화를 활용하여 네이버 기사를 분석한 예시입니다.
예시에서 활용한 파이 차트 및 바 차트, 네트워트 분석은 다음 포스팅에서 진행하겠습니다!!