jeongsoo commited on
Commit
5ccf0d4
·
1 Parent(s): 4e34450

Initial commit

Browse files
Files changed (9) hide show
  1. .gitignore +41 -0
  2. Procfile +1 -0
  3. README.md +48 -0
  4. app.py +338 -0
  5. data/child_mind_data.json +168 -0
  6. huggingface-metadata.json +11 -0
  7. main.py +327 -0
  8. requirements.txt +9 -0
  9. 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>