Spaces:
Sleeping
Sleeping
Initial commit
Browse files- .gitignore +41 -0
- Procfile +1 -0
- README.md +48 -0
- app.py +338 -0
- data/child_mind_data.json +168 -0
- huggingface-metadata.json +11 -0
- main.py +327 -0
- requirements.txt +9 -0
- templates/index.html +197 -0
.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual Environment
|
| 24 |
+
venv/
|
| 25 |
+
env/
|
| 26 |
+
ENV/
|
| 27 |
+
|
| 28 |
+
# IDE
|
| 29 |
+
.idea/
|
| 30 |
+
.vscode/
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
|
| 34 |
+
# Logs
|
| 35 |
+
*.log
|
| 36 |
+
|
| 37 |
+
# Cached data
|
| 38 |
+
*.pickle
|
| 39 |
+
*.pkl
|
| 40 |
+
*.npy
|
| 41 |
+
*.npz
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: gunicorn app:app
|
README.md
CHANGED
|
@@ -11,3 +11,51 @@ license: mit
|
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 14 |
+
|
| 15 |
+
# 한국어 단어 의미 네트워크 시각화
|
| 16 |
+
|
| 17 |
+
이 프로젝트는 한국어 단어들 간의 의미적 관계를 3D 공간에서 시각화하는 웹 애플리케이션입니다.
|
| 18 |
+
|
| 19 |
+
## 주요 기능
|
| 20 |
+
|
| 21 |
+
- BGE-M3 다국어 임베딩 모델을 사용한 단어 임베딩 생성
|
| 22 |
+
- t-SNE 알고리즘을 통한 차원 축소
|
| 23 |
+
- 코사인 유사도 기반 단어 간 관계 분석
|
| 24 |
+
- Plotly 라이브러리를 활용한 인터랙티브 3D 시각화
|
| 25 |
+
- 유사도 임계값 조절 기능
|
| 26 |
+
|
| 27 |
+
## 기술 스택
|
| 28 |
+
|
| 29 |
+
- **백엔드**: Flask
|
| 30 |
+
- **프론트엔드**: Vue.js, Bootstrap 5
|
| 31 |
+
- **데이터 처리**: SentenceTransformers, scikit-learn, numpy
|
| 32 |
+
- **시각화**: Plotly, NetworkX
|
| 33 |
+
|
| 34 |
+
## 사용 방법
|
| 35 |
+
|
| 36 |
+
1. 웹 인터페이스에서 유사도 임계값을 슬라이더로 조절합니다.
|
| 37 |
+
2. "그래프 생성" 버튼을 클릭하여 시각화를 업데이트합니다.
|
| 38 |
+
3. 마우스로 3D 그래프를 회전, 확대/축소하며 단어 간 관계를 탐색합니다.
|
| 39 |
+
4. 단어에 마우스를 올리면 해당 단어와 연결된 다른 단어들의 정보를 확인할 수 있습니다.
|
| 40 |
+
|
| 41 |
+
## 시각화 해석
|
| 42 |
+
|
| 43 |
+
- **위치**: 의미적으로 유사한 단어들은 3D 공간에서 서로 가까이 위치합니다.
|
| 44 |
+
- **엣지(연결선)**: 코사인 유사도가 임계값을 넘는 단어 쌍을 연결합니다.
|
| 45 |
+
- **색상**: Z축 값에 따라 색상이 달라지며, 유사한 색상의 단어들은 Z축 방향으로 유사한 의미를 갖습니다.
|
| 46 |
+
|
| 47 |
+
## 로컬에서 실행하기
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
pip install -r requirements.txt
|
| 51 |
+
python app.py
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## 참고
|
| 55 |
+
|
| 56 |
+
이 애플리케이션은 허깅페이스 Spaces에 배포되어 있으며, 웹 브라우저에서 바로 사용할 수 있습니다.
|
| 57 |
+
BGE-M3 임베딩 모델은 다국어 지원에 최적화되어 있어 한국어 단어 간의 의미적 관계를 효과적으로 분석합니다.
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
© 2025 한국어 단어 의미 네트워크 시각화 프로젝트
|
app.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from flask import Flask, render_template, request, jsonify, send_from_directory
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
from sklearn.manifold import TSNE
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
import matplotlib.font_manager as fm
|
| 9 |
+
import numpy as np
|
| 10 |
+
import platform
|
| 11 |
+
import networkx as nx
|
| 12 |
+
import plotly.graph_objects as go
|
| 13 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 14 |
+
import plotly
|
| 15 |
+
import joblib
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
app = Flask(__name__)
|
| 20 |
+
|
| 21 |
+
# 모델 및 임베딩 캐시를 위한 변수들
|
| 22 |
+
model = None
|
| 23 |
+
model_name = 'BAAI/bge-m3'
|
| 24 |
+
embeddings_cache = {}
|
| 25 |
+
graph_cache = {}
|
| 26 |
+
|
| 27 |
+
# --- 한글 폰트 설정 함수 ---
|
| 28 |
+
def set_korean_font():
|
| 29 |
+
"""
|
| 30 |
+
현재 운영체제에 맞는 한글 폰트를 matplotlib 및 Plotly용으로 설정 시도하고,
|
| 31 |
+
Plotly에서 사용할 폰트 이름을 반환합니다.
|
| 32 |
+
"""
|
| 33 |
+
system_name = platform.system()
|
| 34 |
+
plotly_font_name = None # Plotly에서 사용할 폰트 이름
|
| 35 |
+
|
| 36 |
+
# Matplotlib 폰트 설정
|
| 37 |
+
if system_name == "Windows":
|
| 38 |
+
font_name = "Malgun Gothic"
|
| 39 |
+
plotly_font_name = "Malgun Gothic"
|
| 40 |
+
elif system_name == "Darwin": # MacOS
|
| 41 |
+
font_name = "AppleGothic"
|
| 42 |
+
plotly_font_name = "AppleGothic"
|
| 43 |
+
elif system_name == "Linux":
|
| 44 |
+
# Linux에서 선호하는 한글 폰트 경로 또는 이름 설정
|
| 45 |
+
font_path = "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"
|
| 46 |
+
plotly_font_name_linux = "NanumGothic" # Plotly는 폰트 '이름'을 주로 사용
|
| 47 |
+
|
| 48 |
+
if os.path.exists(font_path):
|
| 49 |
+
font_name = fm.FontProperties(fname=font_path).get_name()
|
| 50 |
+
plotly_font_name = plotly_font_name_linux
|
| 51 |
+
else:
|
| 52 |
+
# 시스템에서 'Nanum' 포함 폰트 찾기 시도
|
| 53 |
+
try:
|
| 54 |
+
available_fonts = [f.name for f in fm.fontManager.ttflist]
|
| 55 |
+
nanum_fonts = [name for name in available_fonts if 'Nanum' in name]
|
| 56 |
+
if nanum_fonts:
|
| 57 |
+
font_name = nanum_fonts[0]
|
| 58 |
+
# Plotly에서 사용할 이름도 비슷하게 설정 (정확한 이름은 시스템마다 다를 수 있음)
|
| 59 |
+
plotly_font_name = font_name if 'Nanum' in font_name else plotly_font_name_linux
|
| 60 |
+
else:
|
| 61 |
+
# 다른 OS 폰트 시도
|
| 62 |
+
if "Malgun Gothic" in available_fonts:
|
| 63 |
+
font_name = "Malgun Gothic"
|
| 64 |
+
plotly_font_name = "Malgun Gothic"
|
| 65 |
+
elif "AppleGothic" in available_fonts:
|
| 66 |
+
font_name = "AppleGothic"
|
| 67 |
+
plotly_font_name = "AppleGothic"
|
| 68 |
+
else:
|
| 69 |
+
font_name = None
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
font_name = None
|
| 73 |
+
|
| 74 |
+
if not font_name:
|
| 75 |
+
font_name = None
|
| 76 |
+
plotly_font_name = None # Plotly도 기본값 사용
|
| 77 |
+
|
| 78 |
+
else: # 기타 OS
|
| 79 |
+
font_name = None
|
| 80 |
+
plotly_font_name = None
|
| 81 |
+
|
| 82 |
+
# Matplotlib 폰트 설정 적용
|
| 83 |
+
if font_name:
|
| 84 |
+
try:
|
| 85 |
+
plt.rc('font', family=font_name)
|
| 86 |
+
plt.rc('axes', unicode_minus=False)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
plt.rcdefaults()
|
| 89 |
+
plt.rc('axes', unicode_minus=False)
|
| 90 |
+
else:
|
| 91 |
+
plt.rcdefaults()
|
| 92 |
+
plt.rc('axes', unicode_minus=False)
|
| 93 |
+
|
| 94 |
+
if not plotly_font_name:
|
| 95 |
+
plotly_font_name = 'sans-serif' # Plotly 기본값 지정
|
| 96 |
+
|
| 97 |
+
return plotly_font_name # Plotly에서 사용할 폰트 이름 반환
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# --- 데이터 로드 함수 ---
|
| 101 |
+
def load_words_from_json(filepath):
|
| 102 |
+
""" JSON 파일에서 'word' 필드만 리스트로 로드합니다. """
|
| 103 |
+
try:
|
| 104 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 105 |
+
data = json.load(f)
|
| 106 |
+
# data가 리스트 형태라고 가정
|
| 107 |
+
if isinstance(data, list):
|
| 108 |
+
words = [item.get('word', '') for item in data if item.get('word')]
|
| 109 |
+
# 빈 문자열 제거
|
| 110 |
+
words = [word for word in words if word]
|
| 111 |
+
return words
|
| 112 |
+
else:
|
| 113 |
+
print(f"오류: 파일 '{filepath}'의 최상위 형식이 리스트가 아닙니다.")
|
| 114 |
+
return None
|
| 115 |
+
except FileNotFoundError:
|
| 116 |
+
print(f"오류: 파일 '{filepath}'를 찾을 수 없습니다.")
|
| 117 |
+
return None
|
| 118 |
+
except json.JSONDecodeError:
|
| 119 |
+
print(f"오류: 파일 '{filepath}'의 JSON 형식이 잘못되었습니다.")
|
| 120 |
+
return None
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"데이터 로딩 중 오류 발생: {e}")
|
| 123 |
+
return None
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def generate_graph(data_file_path='child_mind_data.json', similarity_threshold=0.7):
|
| 127 |
+
"""그래프 생성 함수"""
|
| 128 |
+
global model, model_name, embeddings_cache, graph_cache
|
| 129 |
+
|
| 130 |
+
# 그래프 캐시 확인
|
| 131 |
+
cache_key = f"{data_file_path}_{similarity_threshold}"
|
| 132 |
+
if cache_key in graph_cache:
|
| 133 |
+
return graph_cache[cache_key]
|
| 134 |
+
|
| 135 |
+
# 한글 폰트 설정
|
| 136 |
+
plotly_font = set_korean_font()
|
| 137 |
+
|
| 138 |
+
# 데이터 로드
|
| 139 |
+
word_list = load_words_from_json(data_file_path)
|
| 140 |
+
|
| 141 |
+
if not word_list:
|
| 142 |
+
return {"error": "데이터를 로드할 수 없습니다."}
|
| 143 |
+
|
| 144 |
+
# 중복 제거
|
| 145 |
+
word_list = sorted(list(set(word_list)))
|
| 146 |
+
|
| 147 |
+
# 임베딩 모델 초기화 (최초 1회)
|
| 148 |
+
if model is None:
|
| 149 |
+
try:
|
| 150 |
+
model = SentenceTransformer(model_name)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
return {"error": f"모델 로딩 실패: {e}"}
|
| 153 |
+
|
| 154 |
+
# 임베딩 생성 (캐시 활용)
|
| 155 |
+
if data_file_path in embeddings_cache:
|
| 156 |
+
embeddings = embeddings_cache[data_file_path]
|
| 157 |
+
else:
|
| 158 |
+
try:
|
| 159 |
+
embeddings = model.encode(word_list, show_progress_bar=True, normalize_embeddings=True)
|
| 160 |
+
# 임베딩 캐시 저장
|
| 161 |
+
embeddings_cache[data_file_path] = embeddings
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return {"error": f"임베딩 생성 실패: {e}"}
|
| 164 |
+
|
| 165 |
+
# 3D 좌표 생성 - t-SNE 사용
|
| 166 |
+
effective_perplexity = min(30, len(word_list) - 1)
|
| 167 |
+
if effective_perplexity <= 0:
|
| 168 |
+
effective_perplexity = 5 # 매우 작은 데이터셋 대비
|
| 169 |
+
|
| 170 |
+
try:
|
| 171 |
+
tsne = TSNE(n_components=3, random_state=42, perplexity=effective_perplexity, max_iter=1000, init='pca', learning_rate='auto')
|
| 172 |
+
embeddings_3d = tsne.fit_transform(embeddings)
|
| 173 |
+
except Exception as e:
|
| 174 |
+
return {"error": f"t-SNE 차원 축소 실패: {e}"}
|
| 175 |
+
|
| 176 |
+
# 유사도 계산 및 엣지 정의
|
| 177 |
+
try:
|
| 178 |
+
similarity_matrix = cosine_similarity(embeddings)
|
| 179 |
+
except Exception as e:
|
| 180 |
+
return {"error": f"유사도 계산 실패: {e}"}
|
| 181 |
+
|
| 182 |
+
edges = []
|
| 183 |
+
edge_weights = []
|
| 184 |
+
for i in range(len(word_list)):
|
| 185 |
+
for j in range(i + 1, len(word_list)):
|
| 186 |
+
similarity = similarity_matrix[i, j]
|
| 187 |
+
if similarity > similarity_threshold:
|
| 188 |
+
edges.append((word_list[i], word_list[j]))
|
| 189 |
+
edge_weights.append(similarity)
|
| 190 |
+
|
| 191 |
+
# NetworkX 그래프 생성
|
| 192 |
+
G = nx.Graph()
|
| 193 |
+
for i, word in enumerate(word_list):
|
| 194 |
+
G.add_node(word, pos=(embeddings_3d[i, 0], embeddings_3d[i, 1], embeddings_3d[i, 2]))
|
| 195 |
+
|
| 196 |
+
# 엣지와 가중치 추가
|
| 197 |
+
for edge, weight in zip(edges, edge_weights):
|
| 198 |
+
G.add_edge(edge[0], edge[1], weight=weight)
|
| 199 |
+
|
| 200 |
+
# Plotly 그래프 생성
|
| 201 |
+
# 엣지 좌표 추출
|
| 202 |
+
edge_x = []
|
| 203 |
+
edge_y = []
|
| 204 |
+
edge_z = []
|
| 205 |
+
if edges:
|
| 206 |
+
for edge in G.edges():
|
| 207 |
+
x0, y0, z0 = G.nodes[edge[0]]['pos']
|
| 208 |
+
x1, y1, z1 = G.nodes[edge[1]]['pos']
|
| 209 |
+
edge_x.extend([x0, x1, None])
|
| 210 |
+
edge_y.extend([y0, y1, None])
|
| 211 |
+
edge_z.extend([z0, z1, None])
|
| 212 |
+
|
| 213 |
+
# 엣지용 Scatter3d 트레이스 생성
|
| 214 |
+
edge_trace = go.Scatter3d(
|
| 215 |
+
x=edge_x, y=edge_y, z=edge_z,
|
| 216 |
+
mode='lines',
|
| 217 |
+
line=dict(width=1, color='#888'),
|
| 218 |
+
hoverinfo='none'
|
| 219 |
+
)
|
| 220 |
+
else:
|
| 221 |
+
edge_trace = go.Scatter3d(x=[], y=[], z=[], mode='lines')
|
| 222 |
+
|
| 223 |
+
# 노드 위치와 텍스트 추출
|
| 224 |
+
node_x = [G.nodes[node]['pos'][0] for node in G.nodes()]
|
| 225 |
+
node_y = [G.nodes[node]['pos'][1] for node in G.nodes()]
|
| 226 |
+
node_z = [G.nodes[node]['pos'][2] for node in G.nodes()]
|
| 227 |
+
node_text = list(G.nodes())
|
| 228 |
+
node_adjacencies = []
|
| 229 |
+
node_hover_text = []
|
| 230 |
+
for node, adjacencies in enumerate(G.adjacency()):
|
| 231 |
+
num_connections = len(adjacencies[1])
|
| 232 |
+
node_adjacencies.append(num_connections)
|
| 233 |
+
node_hover_text.append(f'{node_text[node]}<br>연결: {num_connections}개')
|
| 234 |
+
|
| 235 |
+
# 노드용 Scatter3d 트레이스 생성
|
| 236 |
+
node_trace = go.Scatter3d(
|
| 237 |
+
x=node_x, y=node_y, z=node_z,
|
| 238 |
+
mode='markers+text',
|
| 239 |
+
text=node_text,
|
| 240 |
+
hovertext=node_hover_text,
|
| 241 |
+
hoverinfo='text',
|
| 242 |
+
textposition='top center',
|
| 243 |
+
textfont=dict(
|
| 244 |
+
size=10,
|
| 245 |
+
color='black',
|
| 246 |
+
family=plotly_font
|
| 247 |
+
),
|
| 248 |
+
marker=dict(
|
| 249 |
+
size=6,
|
| 250 |
+
color=node_z,
|
| 251 |
+
colorscale='Viridis',
|
| 252 |
+
opacity=0.9,
|
| 253 |
+
colorbar=dict(thickness=15, title='Node Depth (Z-axis)', xanchor='left', title_side='right')
|
| 254 |
+
)
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# 레이아웃 설정
|
| 258 |
+
layout = go.Layout(
|
| 259 |
+
title=dict(
|
| 260 |
+
text=f'어휘 의미 유사성 기반 3D 그래프 (BGE-M3, Threshold: {similarity_threshold})',
|
| 261 |
+
font=dict(size=16, family=plotly_font)
|
| 262 |
+
),
|
| 263 |
+
showlegend=False,
|
| 264 |
+
hovermode='closest',
|
| 265 |
+
margin=dict(b=20, l=5, r=5, t=40),
|
| 266 |
+
scene=dict(
|
| 267 |
+
xaxis=dict(title='TSNE Dimension 1', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 268 |
+
yaxis=dict(title='TSNE Dimension 2', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 269 |
+
zaxis=dict(title='TSNE Dimension 3', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 270 |
+
aspectratio=dict(x=1, y=1, z=0.8)
|
| 271 |
+
)
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Figure 생성
|
| 275 |
+
fig = go.Figure(data=[edge_trace, node_trace], layout=layout)
|
| 276 |
+
|
| 277 |
+
# Plotly JSON 변환
|
| 278 |
+
graph_json = plotly.io.to_json(fig)
|
| 279 |
+
|
| 280 |
+
# 결과 캐시 저장
|
| 281 |
+
graph_cache[cache_key] = graph_json
|
| 282 |
+
|
| 283 |
+
return graph_json
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# 메인 페이지
|
| 287 |
+
@app.route('/')
|
| 288 |
+
def index():
|
| 289 |
+
return render_template('index.html')
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# 그래프 생성 API
|
| 293 |
+
@app.route('/generate-graph', methods=['POST'])
|
| 294 |
+
def create_graph():
|
| 295 |
+
try:
|
| 296 |
+
data = request.json
|
| 297 |
+
threshold = float(data.get('threshold', 0.7))
|
| 298 |
+
use_default_data = data.get('use_default_data', True)
|
| 299 |
+
|
| 300 |
+
# 사용자가 업로드한 데이터 또는 기본 데이터 사용
|
| 301 |
+
if use_default_data:
|
| 302 |
+
data_file = 'child_mind_data.json'
|
| 303 |
+
else:
|
| 304 |
+
# 여기서는 예시로 default만 처리합니다
|
| 305 |
+
# 실제로는 업로드된 파일을 처리하는 코드가 필요합니다
|
| 306 |
+
data_file = 'child_mind_data.json'
|
| 307 |
+
|
| 308 |
+
graph_json = generate_graph(data_file, threshold)
|
| 309 |
+
return graph_json
|
| 310 |
+
|
| 311 |
+
except Exception as e:
|
| 312 |
+
return jsonify({'error': str(e)}), 500
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# 기본 데이터 정보 제공 API
|
| 316 |
+
@app.route('/data-info')
|
| 317 |
+
def get_data_info():
|
| 318 |
+
try:
|
| 319 |
+
data_file = 'child_mind_data.json'
|
| 320 |
+
words = load_words_from_json(data_file)
|
| 321 |
+
|
| 322 |
+
if words:
|
| 323 |
+
return jsonify({
|
| 324 |
+
'wordCount': len(words),
|
| 325 |
+
'sampleWords': words[:10] if len(words) > 10 else words
|
| 326 |
+
})
|
| 327 |
+
else:
|
| 328 |
+
return jsonify({'error': '데이터를 로드할 수 없습니다.'}), 400
|
| 329 |
+
|
| 330 |
+
except Exception as e:
|
| 331 |
+
return jsonify({'error': str(e)}), 500
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
if __name__ == '__main__':
|
| 335 |
+
# 허깅페이스 스페이스에서는 app.run() 대신 app 변수를 노출합니다
|
| 336 |
+
# 로컬 개발용
|
| 337 |
+
# app.run(debug=True, host='0.0.0.0', port=7860)
|
| 338 |
+
pass
|
data/child_mind_data.json
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"word": "학교"},
|
| 3 |
+
{"word": "선생님"},
|
| 4 |
+
{"word": "친구"},
|
| 5 |
+
{"word": "숙제"},
|
| 6 |
+
{"word": "책"},
|
| 7 |
+
{"word": "공부"},
|
| 8 |
+
{"word": "운동장"},
|
| 9 |
+
{"word": "놀이"},
|
| 10 |
+
{"word": "점심"},
|
| 11 |
+
{"word": "쉬는시간"},
|
| 12 |
+
{"word": "집"},
|
| 13 |
+
{"word": "가족"},
|
| 14 |
+
{"word": "엄마"},
|
| 15 |
+
{"word": "아빠"},
|
| 16 |
+
{"word": "형"},
|
| 17 |
+
{"word": "누나"},
|
| 18 |
+
{"word": "동생"},
|
| 19 |
+
{"word": "할머니"},
|
| 20 |
+
{"word": "할아버지"},
|
| 21 |
+
{"word": "밥"},
|
| 22 |
+
{"word": "빵"},
|
| 23 |
+
{"word": "우유"},
|
| 24 |
+
{"word": "과자"},
|
| 25 |
+
{"word": "장난감"},
|
| 26 |
+
{"word": "인형"},
|
| 27 |
+
{"word": "게임"},
|
| 28 |
+
{"word": "축구"},
|
| 29 |
+
{"word": "농구"},
|
| 30 |
+
{"word": "수영"},
|
| 31 |
+
{"word": "피아노"},
|
| 32 |
+
{"word": "기타"},
|
| 33 |
+
{"word": "강아지"},
|
| 34 |
+
{"word": "고양이"},
|
| 35 |
+
{"word": "새"},
|
| 36 |
+
{"word": "물고기"},
|
| 37 |
+
{"word": "해"},
|
| 38 |
+
{"word": "비"},
|
| 39 |
+
{"word": "바람"},
|
| 40 |
+
{"word": "눈"},
|
| 41 |
+
{"word": "봄"},
|
| 42 |
+
{"word": "여름"},
|
| 43 |
+
{"word": "가을"},
|
| 44 |
+
{"word": "겨울"},
|
| 45 |
+
{"word": "크다"},
|
| 46 |
+
{"word": "작다"},
|
| 47 |
+
{"word": "많다"},
|
| 48 |
+
{"word": "적다"},
|
| 49 |
+
{"word": "빠르다"},
|
| 50 |
+
{"word": "느리다"},
|
| 51 |
+
{"word": "재미있다"},
|
| 52 |
+
{"word": "어렵다"},
|
| 53 |
+
{"word": "쉽다"},
|
| 54 |
+
{"word": "예쁘다"},
|
| 55 |
+
{"word": "멋있다"},
|
| 56 |
+
{"word": "좋다"},
|
| 57 |
+
{"word": "나쁘다"},
|
| 58 |
+
{"word": "슬프다"},
|
| 59 |
+
{"word": "기쁘다"},
|
| 60 |
+
{"word": "뜨겁다"},
|
| 61 |
+
{"word": "차갑다"},
|
| 62 |
+
{"word": "무겁다"},
|
| 63 |
+
{"word": "가볍다"},
|
| 64 |
+
{"word": "가다"},
|
| 65 |
+
{"word": "오다"},
|
| 66 |
+
{"word": "먹다"},
|
| 67 |
+
{"word": "마시다"},
|
| 68 |
+
{"word": "자다"},
|
| 69 |
+
{"word": "일어나다"},
|
| 70 |
+
{"word": "놀다"},
|
| 71 |
+
{"word": "공부하다"},
|
| 72 |
+
{"word": "읽다"},
|
| 73 |
+
{"word": "쓰다"},
|
| 74 |
+
{"word": "그리다"},
|
| 75 |
+
{"word": "만들다"},
|
| 76 |
+
{"word": "보다"},
|
| 77 |
+
{"word": "듣다"},
|
| 78 |
+
{"word": "말하다"},
|
| 79 |
+
{"word": "웃다"},
|
| 80 |
+
{"word": "울다"},
|
| 81 |
+
{"word": "뛰다"},
|
| 82 |
+
{"word": "던지다"},
|
| 83 |
+
{"word": "잡다"},
|
| 84 |
+
{"word": "생각하다"},
|
| 85 |
+
{"word": "알다"},
|
| 86 |
+
{"word": "모르다"},
|
| 87 |
+
{"word": "좋아하다"},
|
| 88 |
+
{"word": "싫어하다"},
|
| 89 |
+
{"word": "국어"},
|
| 90 |
+
{"word": "수학"},
|
| 91 |
+
{"word": "영어"},
|
| 92 |
+
{"word": "과학"},
|
| 93 |
+
{"word": "사회"},
|
| 94 |
+
{"word": "음악"},
|
| 95 |
+
{"word": "미술"},
|
| 96 |
+
{"word": "체육"},
|
| 97 |
+
{"word": "숫자"},
|
| 98 |
+
{"word": "더하기"},
|
| 99 |
+
{"word": "빼기"},
|
| 100 |
+
{"word": "곱하기"},
|
| 101 |
+
{"word": "나누기"},
|
| 102 |
+
{"word": "분수"},
|
| 103 |
+
{"word": "도형"},
|
| 104 |
+
{"word": "색깔"},
|
| 105 |
+
{"word": "역사"},
|
| 106 |
+
{"word": "나라"},
|
| 107 |
+
{"word": "식물"},
|
| 108 |
+
{"word": "동물"},
|
| 109 |
+
{"word": "소리"},
|
| 110 |
+
{"word": "발표"},
|
| 111 |
+
{"word": "토론"},
|
| 112 |
+
{"word": "실험"},
|
| 113 |
+
{"word": "시험"},
|
| 114 |
+
{"word": "평가"},
|
| 115 |
+
{"word": "행복"},
|
| 116 |
+
{"word": "슬픔"},
|
| 117 |
+
{"word": "분노"},
|
| 118 |
+
{"word": "두려움"},
|
| 119 |
+
{"word": "놀람"},
|
| 120 |
+
{"word": "즐거움"},
|
| 121 |
+
{"word": "짜증"},
|
| 122 |
+
{"word": "질투"},
|
| 123 |
+
{"word": "시간"},
|
| 124 |
+
{"word": "공간"},
|
| 125 |
+
{"word": "규칙"},
|
| 126 |
+
{"word": "약속"},
|
| 127 |
+
{"word": "노력"},
|
| 128 |
+
{"word": "결과"},
|
| 129 |
+
{"word": "이유"},
|
| 130 |
+
{"word": "방법"},
|
| 131 |
+
{"word": "중요하다"},
|
| 132 |
+
{"word": "권리"},
|
| 133 |
+
{"word": "의무"},
|
| 134 |
+
{"word": "텔레비전"},
|
| 135 |
+
{"word": "컴퓨터"},
|
| 136 |
+
{"word": "휴대폰"},
|
| 137 |
+
{"word": "돈"},
|
| 138 |
+
{"word": "용돈"},
|
| 139 |
+
{"word": "선물"},
|
| 140 |
+
{"word": "옷"},
|
| 141 |
+
{"word": "신발"},
|
| 142 |
+
{"word": "부드럽다"},
|
| 143 |
+
{"word": "귀엽다"},
|
| 144 |
+
{"word": "멋지다"},
|
| 145 |
+
{"word": "조용하다"},
|
| 146 |
+
{"word": "크게"},
|
| 147 |
+
{"word": "작게"},
|
| 148 |
+
{"word": "빨리"},
|
| 149 |
+
{"word": "천천히"},
|
| 150 |
+
{"word": "자주"},
|
| 151 |
+
{"word": "가끔"},
|
| 152 |
+
{"word": "항상"},
|
| 153 |
+
{"word": "보통"},
|
| 154 |
+
{"word": "갑자기"},
|
| 155 |
+
{"word": "그리고"},
|
| 156 |
+
{"word": "그러나"},
|
| 157 |
+
{"word": "그래서"},
|
| 158 |
+
{"word": "왜냐하면"},
|
| 159 |
+
{"word": "~은"},
|
| 160 |
+
{"word": "~는"},
|
| 161 |
+
{"word": "~이"},
|
| 162 |
+
{"word": "~가"},
|
| 163 |
+
{"word": "~을"},
|
| 164 |
+
{"word": "~를"},
|
| 165 |
+
{"word": "~에게"},
|
| 166 |
+
{"word": "~와"},
|
| 167 |
+
{"word": "~과"}
|
| 168 |
+
]
|
huggingface-metadata.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"title": "한국어 단어 의미 네트워크 시각화",
|
| 3 |
+
"emoji": "🌐",
|
| 4 |
+
"colorFrom": "indigo",
|
| 5 |
+
"colorTo": "purple",
|
| 6 |
+
"sdk": "docker",
|
| 7 |
+
"app_port": 7860,
|
| 8 |
+
"app_file": "app.py",
|
| 9 |
+
"pinned": false,
|
| 10 |
+
"license": "mit"
|
| 11 |
+
}
|
main.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from sentence_transformers import SentenceTransformer
|
| 3 |
+
from sklearn.manifold import TSNE
|
| 4 |
+
import matplotlib.pyplot as plt # matplotlib은 폰트 설정 로직에 필요
|
| 5 |
+
import matplotlib.font_manager as fm
|
| 6 |
+
import numpy as np
|
| 7 |
+
import platform
|
| 8 |
+
import os
|
| 9 |
+
import networkx as nx # 그래프 구조 생성
|
| 10 |
+
import plotly.graph_objects as go # 3D 시각화
|
| 11 |
+
from sklearn.metrics.pairwise import cosine_similarity # 유사도 계산
|
| 12 |
+
|
| 13 |
+
# --- 한글 폰트 설정 함수 ---
|
| 14 |
+
def set_korean_font():
|
| 15 |
+
"""
|
| 16 |
+
현재 운영체제에 맞는 한글 폰트를 matplotlib 및 Plotly용으로 설정 시도하고,
|
| 17 |
+
Plotly에서 사용할 폰트 이름을 반환합니다.
|
| 18 |
+
"""
|
| 19 |
+
system_name = platform.system()
|
| 20 |
+
plotly_font_name = None # Plotly에서 사용할 폰트 이름
|
| 21 |
+
|
| 22 |
+
# Matplotlib 폰트 설정
|
| 23 |
+
if system_name == "Windows":
|
| 24 |
+
font_name = "Malgun Gothic"
|
| 25 |
+
plotly_font_name = "Malgun Gothic"
|
| 26 |
+
elif system_name == "Darwin": # MacOS
|
| 27 |
+
font_name = "AppleGothic"
|
| 28 |
+
plotly_font_name = "AppleGothic"
|
| 29 |
+
elif system_name == "Linux":
|
| 30 |
+
# Linux에서 선호하는 한글 폰트 경로 또는 이름 설정
|
| 31 |
+
font_path = "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"
|
| 32 |
+
plotly_font_name_linux = "NanumGothic" # Plotly는 폰트 '이름'을 주로 사용
|
| 33 |
+
|
| 34 |
+
if os.path.exists(font_path):
|
| 35 |
+
font_name = fm.FontProperties(fname=font_path).get_name()
|
| 36 |
+
plotly_font_name = plotly_font_name_linux
|
| 37 |
+
print(f"Using font: {font_name} from {font_path}")
|
| 38 |
+
else:
|
| 39 |
+
# 시스템에서 'Nanum' 포함 폰트 찾기 시도
|
| 40 |
+
try:
|
| 41 |
+
available_fonts = [f.name for f in fm.fontManager.ttflist]
|
| 42 |
+
nanum_fonts = [name for name in available_fonts if 'Nanum' in name]
|
| 43 |
+
if nanum_fonts:
|
| 44 |
+
font_name = nanum_fonts[0]
|
| 45 |
+
# Plotly에서 사용할 이름도 비슷하게 설정 (정확한 이름은 시스템마다 다를 수 있음)
|
| 46 |
+
plotly_font_name = font_name if 'Nanum' in font_name else plotly_font_name_linux
|
| 47 |
+
print(f"Found and using system font: {font_name}")
|
| 48 |
+
else:
|
| 49 |
+
# 다른 OS 폰트 시도
|
| 50 |
+
if "Malgun Gothic" in available_fonts:
|
| 51 |
+
font_name = "Malgun Gothic"
|
| 52 |
+
plotly_font_name = "Malgun Gothic"
|
| 53 |
+
elif "AppleGothic" in available_fonts:
|
| 54 |
+
font_name = "AppleGothic"
|
| 55 |
+
plotly_font_name = "AppleGothic"
|
| 56 |
+
else:
|
| 57 |
+
font_name = None
|
| 58 |
+
if font_name: print(f"Trying fallback font: {font_name}")
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Error finding Linux font: {e}")
|
| 62 |
+
font_name = None
|
| 63 |
+
|
| 64 |
+
if not font_name:
|
| 65 |
+
print("Warning: Linux 한글 폰트를 자동으로 찾지 못했습니다. Matplotlib 기본 폰트를 사용합니다.")
|
| 66 |
+
font_name = None
|
| 67 |
+
plotly_font_name = None # Plotly도 기본값 사용
|
| 68 |
+
|
| 69 |
+
else: # 기타 OS
|
| 70 |
+
font_name = None
|
| 71 |
+
plotly_font_name = None
|
| 72 |
+
|
| 73 |
+
# Matplotlib 폰트 설정 적용
|
| 74 |
+
if font_name:
|
| 75 |
+
try:
|
| 76 |
+
plt.rc('font', family=font_name)
|
| 77 |
+
plt.rc('axes', unicode_minus=False)
|
| 78 |
+
print(f"Matplotlib font set to: {font_name}")
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"Error setting Matplotlib font '{font_name}': {e}. Using default.")
|
| 81 |
+
plt.rcdefaults()
|
| 82 |
+
plt.rc('axes', unicode_minus=False)
|
| 83 |
+
# Plotly 폰트 이름도 기본값으로 되돌릴 수 있음 (선택적)
|
| 84 |
+
# plotly_font_name = None
|
| 85 |
+
else:
|
| 86 |
+
print("Matplotlib Korean font not set. Using default font.")
|
| 87 |
+
plt.rcdefaults()
|
| 88 |
+
plt.rc('axes', unicode_minus=False)
|
| 89 |
+
|
| 90 |
+
if not plotly_font_name:
|
| 91 |
+
print("Plotly font name not explicitly found, will use Plotly default (sans-serif).")
|
| 92 |
+
plotly_font_name = 'sans-serif' # Plotly 기본값 지정
|
| 93 |
+
|
| 94 |
+
print(f"Plotly will try to use font: {plotly_font_name}")
|
| 95 |
+
return plotly_font_name # Plotly에서 사용할 폰트 이름 반환
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# --- 데이터 로드 함수 ---
|
| 99 |
+
def load_titles_from_json(filepath):
|
| 100 |
+
""" JSON 파일에서 'title'만 리스트로 로드합니다. """
|
| 101 |
+
try:
|
| 102 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 103 |
+
data = json.load(f)
|
| 104 |
+
# data가 리스트 형태라고 가정
|
| 105 |
+
if isinstance(data, list):
|
| 106 |
+
titles = [item.get('word', '') for item in data if item.get('word')]
|
| 107 |
+
# 빈 문자열 제거
|
| 108 |
+
titles = [title for title in titles if title]
|
| 109 |
+
return titles
|
| 110 |
+
else:
|
| 111 |
+
print(f"오류: 파일 '{filepath}'의 최상위 형식이 리스트가 아닙니다.")
|
| 112 |
+
return None
|
| 113 |
+
except FileNotFoundError:
|
| 114 |
+
print(f"오류: 파일 '{filepath}'를 찾을 수 없습니다.")
|
| 115 |
+
return None
|
| 116 |
+
except json.JSONDecodeError:
|
| 117 |
+
print(f"오류: 파일 '{filepath}'의 JSON 형식이 잘못되었습니다.")
|
| 118 |
+
return None
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(f"데이터 로딩 중 오류 발생: {e}")
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# --- 메인 실행 부분 ---
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
# 한글 폰트 설정 (matplotlib용, Plotly용 이름도 받아옴)
|
| 127 |
+
plotly_font = set_korean_font()
|
| 128 |
+
|
| 129 |
+
# --- 설정값 ---
|
| 130 |
+
data_file_path = 'child_mind_data.json' # 입력 데이터 파일 경로
|
| 131 |
+
embedding_model_name = 'BAAI/bge-m3' # 수정: BGE-M3 모델로 변경
|
| 132 |
+
similarity_threshold = 0.7 # 엣지를 생성할 코사인 유사도 임계값 (0.0 ~ 1.0)
|
| 133 |
+
tsne_perplexity = 30 # t-SNE perplexity (데이터 수보다 작아야 함)
|
| 134 |
+
tsne_max_iter = 1000 # t-SNE 반복 횟수
|
| 135 |
+
# ---
|
| 136 |
+
|
| 137 |
+
# 1. 데이터 로드 (어휘 제목 리스트)
|
| 138 |
+
print(f"데이터 로딩 시도: {data_file_path}")
|
| 139 |
+
word_list = load_titles_from_json(data_file_path)
|
| 140 |
+
|
| 141 |
+
if not word_list:
|
| 142 |
+
print("시각화할 어휘 데이터가 없습니다. 프로그램을 종료합니다.")
|
| 143 |
+
exit() # 데이터 없으면 종료
|
| 144 |
+
else:
|
| 145 |
+
print(f"총 {len(word_list)}개의 유효한 어휘를 로드했습니다.")
|
| 146 |
+
# 중복 제거 (선택적)
|
| 147 |
+
original_count = len(word_list)
|
| 148 |
+
word_list = sorted(list(set(word_list)))
|
| 149 |
+
if len(word_list) < original_count:
|
| 150 |
+
print(f"중복 제거 후 {len(word_list)}개의 고유한 어휘가 남았습니다.")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# 2. 임베딩 모델 로드
|
| 154 |
+
print(f"임베딩 모델 로딩 중: {embedding_model_name} ...")
|
| 155 |
+
try:
|
| 156 |
+
model = SentenceTransformer(embedding_model_name)
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"오류: 임베딩 모델 '{embedding_model_name}' 로딩에 실패했습니다. {e}")
|
| 159 |
+
print("인터넷 연결 및 모델 이름을 확인하세요.")
|
| 160 |
+
exit()
|
| 161 |
+
print("모델 로딩 완료.")
|
| 162 |
+
|
| 163 |
+
# 3. 임베딩 생성
|
| 164 |
+
print("어휘 임베딩 생성 중...")
|
| 165 |
+
try:
|
| 166 |
+
# BGE 모델 특화 파라미터 추가
|
| 167 |
+
embeddings = model.encode(word_list, show_progress_bar=True, normalize_embeddings=True)
|
| 168 |
+
except Exception as e:
|
| 169 |
+
print(f"오류: 임베딩 생성 중 문제가 발생했습니다. {e}")
|
| 170 |
+
exit()
|
| 171 |
+
print(f"임베딩 생성 완료. 각 어휘는 {embeddings.shape[1]}차원 벡터로 변환되었습니다.")
|
| 172 |
+
|
| 173 |
+
# 4. 3D 좌표 생성 - t-SNE 사용
|
| 174 |
+
print("3차원 좌표 생성 중 (t-SNE)...")
|
| 175 |
+
# perplexity 값 조정 (데이터 수보다 작아야 함)
|
| 176 |
+
effective_perplexity = min(tsne_perplexity, len(word_list) - 1)
|
| 177 |
+
if effective_perplexity <= 0:
|
| 178 |
+
print(f"Warning: 데이터 수가 너무 적어 ({len(word_list)}개) perplexity를 5로 강제 설정합니다.")
|
| 179 |
+
effective_perplexity = 5 # 매우 작은 데이터셋 대비
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
tsne = TSNE(n_components=3, random_state=42, perplexity=effective_perplexity, max_iter=tsne_max_iter, init='pca', learning_rate='auto')
|
| 183 |
+
embeddings_3d = tsne.fit_transform(embeddings)
|
| 184 |
+
except Exception as e:
|
| 185 |
+
print(f"오류: t-SNE 차원 축소 중 문제가 발생했습니다. {e}")
|
| 186 |
+
exit()
|
| 187 |
+
print("3차원 좌표 생성 완료.")
|
| 188 |
+
|
| 189 |
+
# 5. 유사도 계산 및 엣지 정의
|
| 190 |
+
print("어휘 간 유사도 계산 및 엣지 정의 중...")
|
| 191 |
+
try:
|
| 192 |
+
similarity_matrix = cosine_similarity(embeddings)
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(f"오류: 코사인 유사도 계산 중 문제가 발생했습니다. {e}")
|
| 195 |
+
exit()
|
| 196 |
+
|
| 197 |
+
edges = []
|
| 198 |
+
edge_weights = [] # 엣지 두께 등에 활용할 가중치
|
| 199 |
+
for i in range(len(word_list)):
|
| 200 |
+
for j in range(i + 1, len(word_list)): # 중복 및 자기 자신 연결 방지
|
| 201 |
+
similarity = similarity_matrix[i, j]
|
| 202 |
+
if similarity > similarity_threshold:
|
| 203 |
+
edges.append((word_list[i], word_list[j]))
|
| 204 |
+
edge_weights.append(similarity) # 유사도 값을 가중치로 사용
|
| 205 |
+
print(f"유사도 임계값 ({similarity_threshold}) 초과 엣지 {len(edges)}개 정의 완료.")
|
| 206 |
+
|
| 207 |
+
if not edges:
|
| 208 |
+
print("Warning: 정의된 엣지가 없습니다. 유사도 임계값이 너무 높거나 데이터 간 유사성이 낮을 수 있습니다.")
|
| 209 |
+
# 엣지가 없어도 노드만 표시하도록 계속 진행
|
| 210 |
+
|
| 211 |
+
# 6. NetworkX 그래프 생성
|
| 212 |
+
print("NetworkX 그래프 객체 생성 중...")
|
| 213 |
+
G = nx.Graph()
|
| 214 |
+
for i, word in enumerate(word_list):
|
| 215 |
+
# 노드 속성으로 3D 좌표 저장
|
| 216 |
+
G.add_node(word, pos=(embeddings_3d[i, 0], embeddings_3d[i, 1], embeddings_3d[i, 2]))
|
| 217 |
+
|
| 218 |
+
# 엣지와 가중치 추가
|
| 219 |
+
for edge, weight in zip(edges, edge_weights):
|
| 220 |
+
G.add_edge(edge[0], edge[1], weight=weight)
|
| 221 |
+
print("NetworkX 그래프 생성 완료.")
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# --- Plotly를 사용한 3D 시각화 ---
|
| 225 |
+
print("Plotly 3D 그래프 생성 중...")
|
| 226 |
+
|
| 227 |
+
# 엣지 좌표 추출
|
| 228 |
+
edge_x = []
|
| 229 |
+
edge_y = []
|
| 230 |
+
edge_z = []
|
| 231 |
+
if edges: # 엣지가 있을 경우에만 처리
|
| 232 |
+
for edge in G.edges():
|
| 233 |
+
x0, y0, z0 = G.nodes[edge[0]]['pos']
|
| 234 |
+
x1, y1, z1 = G.nodes[edge[1]]['pos']
|
| 235 |
+
edge_x.extend([x0, x1, None]) # None을 넣어 선을 분리
|
| 236 |
+
edge_y.extend([y0, y1, None])
|
| 237 |
+
edge_z.extend([z0, z1, None])
|
| 238 |
+
|
| 239 |
+
# 엣지용 Scatter3d 트레이스 생성
|
| 240 |
+
edge_trace = go.Scatter3d(
|
| 241 |
+
x=edge_x, y=edge_y, z=edge_z,
|
| 242 |
+
mode='lines',
|
| 243 |
+
line=dict(width=1, color='#888'), # 엣지 색상 및 두께
|
| 244 |
+
hoverinfo='none' # 엣지에는 호버 정보 없음
|
| 245 |
+
)
|
| 246 |
+
else:
|
| 247 |
+
edge_trace = go.Scatter3d(x=[], y=[], z=[], mode='lines') # 엣지 없으면 빈 트레이스
|
| 248 |
+
|
| 249 |
+
# 노드 위치와 텍스트 추출
|
| 250 |
+
node_x = [G.nodes[node]['pos'][0] for node in G.nodes()]
|
| 251 |
+
node_y = [G.nodes[node]['pos'][1] for node in G.nodes()]
|
| 252 |
+
node_z = [G.nodes[node]['pos'][2] for node in G.nodes()]
|
| 253 |
+
node_text = list(G.nodes()) # 노드 이름 (어휘)
|
| 254 |
+
node_adjacencies = [] # 연결된 엣지 수 (마커 크기 등에 활용 가능)
|
| 255 |
+
node_hover_text = [] # 노드 호버 텍스트
|
| 256 |
+
for node, adjacencies in enumerate(G.adjacency()):
|
| 257 |
+
num_connections = len(adjacencies[1])
|
| 258 |
+
node_adjacencies.append(num_connections)
|
| 259 |
+
node_hover_text.append(f'{node_text[node]}<br>Connections: {num_connections}')
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
# 노드용 Scatter3d 트레이스 생성
|
| 263 |
+
node_trace = go.Scatter3d(
|
| 264 |
+
x=node_x, y=node_y, z=node_z,
|
| 265 |
+
mode='markers+text',
|
| 266 |
+
text=node_text,
|
| 267 |
+
hovertext=node_hover_text,
|
| 268 |
+
hoverinfo='text',
|
| 269 |
+
textposition='top center',
|
| 270 |
+
textfont=dict(
|
| 271 |
+
size=10,
|
| 272 |
+
color='black',
|
| 273 |
+
family=plotly_font
|
| 274 |
+
),
|
| 275 |
+
marker=dict(
|
| 276 |
+
size=6,
|
| 277 |
+
color=node_z,
|
| 278 |
+
colorscale='Viridis',
|
| 279 |
+
opacity=0.9,
|
| 280 |
+
colorbar=dict(thickness=15, title='Node Depth (Z-axis)', xanchor='left', title_side='right')
|
| 281 |
+
# titleside → title_side
|
| 282 |
+
)
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
# 레이아웃 설정
|
| 286 |
+
layout = go.Layout(
|
| 287 |
+
title=dict(
|
| 288 |
+
text=f'어휘 의미 유사성 기반 3D 그래프 (BGE-M3, Threshold: {similarity_threshold})',
|
| 289 |
+
font=dict(size=16, family=plotly_font)
|
| 290 |
+
),
|
| 291 |
+
showlegend=False,
|
| 292 |
+
hovermode='closest', # 가장 가까운 데이터 포인트 정보 표시
|
| 293 |
+
margin=dict(b=20, l=5, r=5, t=40), # 여백
|
| 294 |
+
scene=dict( # 3D 씬 설정
|
| 295 |
+
xaxis=dict(title='TSNE Dimension 1', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 296 |
+
yaxis=dict(title='TSNE Dimension 2', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 297 |
+
zaxis=dict(title='TSNE Dimension 3', showticklabels=False, backgroundcolor="rgb(230, 230,230)", gridcolor="white", zerolinecolor="white"),
|
| 298 |
+
aspectratio=dict(x=1, y=1, z=0.8) # 축 비율 조정
|
| 299 |
+
),
|
| 300 |
+
# 주석 추가 (옵션)
|
| 301 |
+
# annotations=[
|
| 302 |
+
# dict(
|
| 303 |
+
# showarrow=False,
|
| 304 |
+
# text=f"Data: {data_file_path}<br>Model: {embedding_model_name}",
|
| 305 |
+
# xref="paper", yref="paper",
|
| 306 |
+
# x=0.005, y=0.005
|
| 307 |
+
# )
|
| 308 |
+
# ]
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# Figure 생성 및 표시
|
| 312 |
+
fig = go.Figure(data=[edge_trace, node_trace], layout=layout)
|
| 313 |
+
|
| 314 |
+
print("*"*20)
|
| 315 |
+
print(" 인터랙티브 3D 그래프를 표시합니다. ")
|
| 316 |
+
print(" - 마우스 휠: 줌 인/아웃")
|
| 317 |
+
print(" - 마우스 드래그: 회전")
|
| 318 |
+
print(" - 노드 위에 마우스 올리기: 어휘 이름 및 연결 수 확인")
|
| 319 |
+
print("*"*20)
|
| 320 |
+
|
| 321 |
+
# HTML 파일로 저장 (선택적)
|
| 322 |
+
# fig.write_html("3d_graph_visualization.html")
|
| 323 |
+
# print("그래프를 '3d_graph_visualization.html' 파일로 저장했습니다.")
|
| 324 |
+
|
| 325 |
+
fig.show() # 웹 브라우저 또는 IDE 출력 창에 표시
|
| 326 |
+
|
| 327 |
+
print("그래프 표시 완료.")
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==2.2.3
|
| 2 |
+
sentence-transformers==2.2.2
|
| 3 |
+
scikit-learn==1.2.2
|
| 4 |
+
numpy==1.24.3
|
| 5 |
+
matplotlib==3.7.1
|
| 6 |
+
networkx==3.1
|
| 7 |
+
plotly==5.14.1
|
| 8 |
+
joblib==1.2.0
|
| 9 |
+
gunicorn==20.1.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>한국어 단어 의미 네트워크 시각화</title>
|
| 7 |
+
<!-- Plotly.js -->
|
| 8 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 9 |
+
<!-- Bootstrap CSS -->
|
| 10 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 11 |
+
<style>
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Malgun Gothic', 'Apple Gothic', 'NanumGothic', sans-serif;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
background-color: #f8f9fa;
|
| 16 |
+
}
|
| 17 |
+
#graphContainer {
|
| 18 |
+
height: 80vh;
|
| 19 |
+
width: 100%;
|
| 20 |
+
border-radius: 8px;
|
| 21 |
+
background-color: #fff;
|
| 22 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 23 |
+
margin-bottom: 20px;
|
| 24 |
+
}
|
| 25 |
+
.controls-container {
|
| 26 |
+
background-color: #fff;
|
| 27 |
+
padding: 20px;
|
| 28 |
+
border-radius: 8px;
|
| 29 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 30 |
+
margin-bottom: 20px;
|
| 31 |
+
}
|
| 32 |
+
.spinner {
|
| 33 |
+
position: absolute;
|
| 34 |
+
top: 50%;
|
| 35 |
+
left: 50%;
|
| 36 |
+
transform: translate(-50%, -50%);
|
| 37 |
+
z-index: 1000;
|
| 38 |
+
}
|
| 39 |
+
.info-section {
|
| 40 |
+
margin-top: 20px;
|
| 41 |
+
background-color: #fff;
|
| 42 |
+
padding: 20px;
|
| 43 |
+
border-radius: 8px;
|
| 44 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 45 |
+
}
|
| 46 |
+
h1, h3 {
|
| 47 |
+
color: #343a40;
|
| 48 |
+
}
|
| 49 |
+
.btn-primary {
|
| 50 |
+
background-color: #5a67d8;
|
| 51 |
+
border-color: #5a67d8;
|
| 52 |
+
}
|
| 53 |
+
.btn-primary:hover {
|
| 54 |
+
background-color: #4c51bf;
|
| 55 |
+
border-color: #4c51bf;
|
| 56 |
+
}
|
| 57 |
+
</style>
|
| 58 |
+
</head>
|
| 59 |
+
<body>
|
| 60 |
+
<div class="container">
|
| 61 |
+
<div class="row mb-4">
|
| 62 |
+
<div class="col-12">
|
| 63 |
+
<h1 class="text-center my-4">한국어 단어 의미 네트워크 시각화</h1>
|
| 64 |
+
<div class="alert alert-info" role="alert">
|
| 65 |
+
이 도구는 한국어 단어들 간의 의미적 관계를 3D 공간에서 시각화합니다. BGE-M3 다국어 임베딩 모델을 사용하여 단어 간 의미적 유사성을 분석합니다.
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="row">
|
| 71 |
+
<div class="col-md-3">
|
| 72 |
+
<div class="controls-container">
|
| 73 |
+
<h3>설정</h3>
|
| 74 |
+
<div class="mb-3">
|
| 75 |
+
<label for="thresholdSlider" class="form-label">유사도 임계값 ({{ threshold }})</label>
|
| 76 |
+
<input type="range" class="form-range" id="thresholdSlider" min="0.1" max="0.9" step="0.05" v-model="threshold">
|
| 77 |
+
<div class="form-text">높은 값 = 더 엄격한 연결 기준 (적은 엣지)</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div class="d-grid gap-2">
|
| 81 |
+
<button class="btn btn-primary" @click="generateGraph" :disabled="isLoading">
|
| 82 |
+
그래프 생성
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div class="info-section">
|
| 87 |
+
<h3>데이터 정보</h3>
|
| 88 |
+
<div v-if="dataInfo">
|
| 89 |
+
<p><strong>단어 수:</strong> {{ dataInfo.wordCount }}</p>
|
| 90 |
+
<p><strong>샘플 단어:</strong></p>
|
| 91 |
+
<div class="text-muted">{{ dataInfo.sampleWords.join(', ') }}</div>
|
| 92 |
+
</div>
|
| 93 |
+
<div v-else>
|
| 94 |
+
<p class="text-muted">데이터 정보를 로드 중입니다...</p>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div class="col-md-9">
|
| 101 |
+
<div id="graphContainer" style="position: relative;">
|
| 102 |
+
<div class="spinner" v-if="isLoading">
|
| 103 |
+
<div class="spinner-border text-primary" role="status">
|
| 104 |
+
<span class="visually-hidden">로딩 중...</span>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div class="alert alert-secondary">
|
| 110 |
+
<h5>조작 방법:</h5>
|
| 111 |
+
<ul>
|
| 112 |
+
<li>마우스 휠: 확대/축소</li>
|
| 113 |
+
<li>마우스 드래그: 회전</li>
|
| 114 |
+
<li>마우스 오른쪽 버튼 드래그: 이동</li>
|
| 115 |
+
<li>단어에 마우스 오버: 상세 정보 확인</li>
|
| 116 |
+
</ul>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<div class="row mt-4">
|
| 122 |
+
<div class="col-12">
|
| 123 |
+
<div class="card">
|
| 124 |
+
<div class="card-body">
|
| 125 |
+
<h3 class="card-title">이 시각화에 대해</h3>
|
| 126 |
+
<p class="card-text">
|
| 127 |
+
이 도구는 다음과 같은 기술을 사용하여 한국어 단어 네트워크를 시각화합니다:
|
| 128 |
+
</p>
|
| 129 |
+
<ul>
|
| 130 |
+
<li><strong>BAAI/bge-m3 임베딩:</strong> 다양한 언어에 최적화된 최신 임베딩 모델로, 한국어에서도 우수한 성능을 보입니다.</li>
|
| 131 |
+
<li><strong>t-SNE 차원 축소:</strong> 복잡한 고차원 벡터를 3D 공간에 투영하여 의미적 관계를 시각화합니다.</li>
|
| 132 |
+
<li><strong>코사인 유사도:</strong> 단어 벡터 간 각도를 기반으로 의미적 유사성을 측정합니다.</li>
|
| 133 |
+
<li><strong>Plotly 시각화:</strong> 인터랙티브한 3D 시각화를 제공합니다.</li>
|
| 134 |
+
</ul>
|
| 135 |
+
<p class="card-text">
|
| 136 |
+
각 단어는 3D 공간의 점으로 표시되며, 유사도가 높은 단어들은 연결선(엣지)으로 연결됩니다. 색상은 z축 값에 따라 다르게 표시됩니다.
|
| 137 |
+
</p>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<!-- Vue.js -->
|
| 145 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.36/dist/vue.global.prod.js"></script>
|
| 146 |
+
<!-- Axios -->
|
| 147 |
+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
| 148 |
+
<script>
|
| 149 |
+
const app = Vue.createApp({
|
| 150 |
+
data() {
|
| 151 |
+
return {
|
| 152 |
+
threshold: 0.7,
|
| 153 |
+
isLoading: false,
|
| 154 |
+
dataInfo: null,
|
| 155 |
+
errorMessage: ''
|
| 156 |
+
}
|
| 157 |
+
},
|
| 158 |
+
mounted() {
|
| 159 |
+
this.loadDataInfo();
|
| 160 |
+
this.generateGraph();
|
| 161 |
+
},
|
| 162 |
+
methods: {
|
| 163 |
+
loadDataInfo() {
|
| 164 |
+
axios.get('/data-info')
|
| 165 |
+
.then(response => {
|
| 166 |
+
this.dataInfo = response.data;
|
| 167 |
+
})
|
| 168 |
+
.catch(error => {
|
| 169 |
+
console.error('데이터 정보 로드 오류:', error);
|
| 170 |
+
});
|
| 171 |
+
},
|
| 172 |
+
generateGraph() {
|
| 173 |
+
this.isLoading = true;
|
| 174 |
+
this.errorMessage = '';
|
| 175 |
+
|
| 176 |
+
axios.post('/generate-graph', {
|
| 177 |
+
threshold: this.threshold,
|
| 178 |
+
use_default_data: true
|
| 179 |
+
})
|
| 180 |
+
.then(response => {
|
| 181 |
+
const graphData = response.data;
|
| 182 |
+
Plotly.newPlot('graphContainer', JSON.parse(graphData));
|
| 183 |
+
this.isLoading = false;
|
| 184 |
+
})
|
| 185 |
+
.catch(error => {
|
| 186 |
+
console.error('그래프 생성 오류:', error);
|
| 187 |
+
this.errorMessage = '그래프 생성 중 오류가 발생했습니다.';
|
| 188 |
+
this.isLoading = false;
|
| 189 |
+
});
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
app.mount('.container');
|
| 195 |
+
</script>
|
| 196 |
+
</body>
|
| 197 |
+
</html>
|