小形克宏 commited on
Commit
2bd094f
·
1 Parent(s): 8fa637a

Initial commit: StructEval-T Analyzer

Browse files
Files changed (4) hide show
  1. .gitignore +5 -0
  2. README.md +57 -8
  3. app.py +446 -0
  4. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
4
+ *.jsonl
5
+ flagged/
README.md CHANGED
@@ -1,14 +1,63 @@
1
  ---
2
- title: Structeval Analyz
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.6.0
8
  app_file: app.py
9
  pinned: false
10
- license: apache-2.0
11
- short_description: 松尾研Deep Learning応用講座2025最終課題のための分析器
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: StructEval-T Analyzer
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: "5.12.0"
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
 
11
  ---
12
 
13
+ # 🔍 StructEval-T Analyzer
14
+
15
+ 松尾研LLM講義2025 メインコンペ用の推論結果分析ツールです。
16
+
17
+ ## 概要
18
+
19
+ `inference.json` と `public_150.json` をアップロードすることで、モデル出力の構文的正確性(パース可能性)やエラーパターンを分析できます。
20
+
21
+ ## 機能
22
+
23
+ ### 📊 構文検証(Syntax Validation)
24
+ 各フォーマット(JSON, YAML, TOML, XML, CSV)ごとにPythonの標準パーサーで構文を検証します。
25
+
26
+ ### ❌ エラーパターン自動分類
27
+ パースに失敗した出力に対して、以下のエラーパターンを自動検出します:
28
+
29
+ | パターン | 説明 |
30
+ |---------|------|
31
+ | `markdown_block` | マークダウンコードブロック(\`\`\`json 等)の混入 |
32
+ | `natural_language_prefix` | 先頭に自然言語("Here is..."等)が混入 |
33
+ | `natural_language_suffix` | 末尾に自然言語("Note:"等)が混入 |
34
+ | `truncation` | 出力の途切れ(閉じ括弧・タグの欠落) |
35
+ | `empty_output` | 空の出力 |
36
+ | `wrong_format` | 要求と異なるフォーマットの出力 |
37
+ | `cot_leakage` | 思考過程(\<think\>等)の混入 |
38
+
39
+ ### 📈 複数実験の比較
40
+ 複数の `inference.json` をアップロードすることで、実験間のパース成功率を比較できます。
41
+
42
+ ## 使い方
43
+
44
+ 1. `public_150.json` をアップロード
45
+ 2. 1つ以上の `inference.json` をアップロード(複数ファイル対応)
46
+ 3. 「分析開始」ボタンをクリック
47
+
48
+ ## 注意事項
49
+
50
+ - このツールは**構文的な正確性(パース可能かどうか)のみ**を検証します
51
+ - 運営側の採点基準である `raw_output_metric`(特定キーの存在チェック等)は再現できません
52
+ - スコアの完全な再現を目的としたものではなく、**エラーの傾向把握**に活用してください
53
+
54
+ ## ローカルでの実行
55
+
56
+ ```bash
57
+ pip install gradio pandas pyyaml
58
+ python app.py
59
+ ```
60
+
61
+ ## ライセンス
62
+
63
+ MIT License
app.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ StructEval-T Analyzer
3
+ 松尾研LLM講義2025 メインコンペ用 推論結果分析ツール
4
+
5
+ inference.json と public_150.json をアップロードして、
6
+ フォーマット別のパース成功率やエラーパターンを分析します。
7
+ """
8
+
9
+ import json
10
+ import csv
11
+ import io
12
+ import re
13
+ import traceback
14
+ from collections import Counter, defaultdict
15
+ from pathlib import Path
16
+
17
+ import gradio as gr
18
+ import pandas as pd
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # 1. Syntax Validators (フォーマット別パーサー)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def validate_json(text: str) -> tuple[bool, str]:
25
+ """JSON構文を検証"""
26
+ try:
27
+ json.loads(text)
28
+ return True, ""
29
+ except json.JSONDecodeError as e:
30
+ return False, f"JSONDecodeError: {e.msg} (line {e.lineno}, col {e.colno})"
31
+
32
+
33
+ def validate_yaml(text: str) -> tuple[bool, str]:
34
+ """YAML構文を検証"""
35
+ try:
36
+ import yaml
37
+ yaml.safe_load(text)
38
+ return True, ""
39
+ except yaml.YAMLError as e:
40
+ return False, f"YAMLError: {e}"
41
+ except Exception as e:
42
+ return False, f"Error: {e}"
43
+
44
+
45
+ def validate_toml(text: str) -> tuple[bool, str]:
46
+ """TOML構文を検証"""
47
+ try:
48
+ import tomllib
49
+ tomllib.loads(text)
50
+ return True, ""
51
+ except Exception as e:
52
+ return False, f"TOMLError: {e}"
53
+
54
+
55
+ def validate_xml(text: str) -> tuple[bool, str]:
56
+ """XML構文を検証"""
57
+ try:
58
+ import xml.etree.ElementTree as ET
59
+ ET.fromstring(text)
60
+ return True, ""
61
+ except ET.ParseError as e:
62
+ return False, f"XMLParseError: {e}"
63
+ except Exception as e:
64
+ return False, f"Error: {e}"
65
+
66
+
67
+ def validate_csv(text: str) -> tuple[bool, str]:
68
+ """CSV構文を検証"""
69
+ try:
70
+ reader = csv.reader(io.StringIO(text))
71
+ rows = list(reader)
72
+ if len(rows) == 0:
73
+ return False, "Empty CSV"
74
+ if len(rows) == 1:
75
+ return False, "CSV has only header, no data rows"
76
+ # 列数の一貫性チェック
77
+ col_counts = [len(row) for row in rows]
78
+ if len(set(col_counts)) > 1:
79
+ return False, f"Inconsistent column counts: {col_counts[:5]}"
80
+ return True, ""
81
+ except Exception as e:
82
+ return False, f"CSVError: {e}"
83
+
84
+
85
+ VALIDATORS = {
86
+ "JSON": validate_json,
87
+ "YAML": validate_yaml,
88
+ "TOML": validate_toml,
89
+ "XML": validate_xml,
90
+ "CSV": validate_csv,
91
+ }
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # 2. Error Pattern Classifier (エラーパターン自動分類)
95
+ # ---------------------------------------------------------------------------
96
+
97
+ def classify_error_patterns(generation: str, output_type: str) -> list[str]:
98
+ """出力テキストのエラーパターンを分類"""
99
+ patterns = []
100
+
101
+ # マークダウンブロックの混入
102
+ if re.search(r"```\w*", generation):
103
+ patterns.append("markdown_block")
104
+
105
+ # 自然言語の混入(先頭部分)
106
+ first_line = generation.strip().split("\n")[0] if generation.strip() else ""
107
+ nl_indicators = [
108
+ "here is", "here's", "below is", "the following",
109
+ "sure", "certainly", "of course", "i'll",
110
+ "let me", "note:", "output:",
111
+ ]
112
+ if any(ind in first_line.lower() for ind in nl_indicators):
113
+ patterns.append("natural_language_prefix")
114
+
115
+ # 末尾の自然言語混入
116
+ last_lines = generation.strip().split("\n")[-3:] if generation.strip() else []
117
+ last_text = " ".join(last_lines).lower()
118
+ nl_suffix = ["note:", "explanation:", "this ", "the above", "please "]
119
+ if any(ind in last_text for ind in nl_suffix):
120
+ patterns.append("natural_language_suffix")
121
+
122
+ # 途切れ(トランケーション)の検出
123
+ stripped = generation.rstrip()
124
+ if output_type == "JSON":
125
+ open_count = generation.count("{") + generation.count("[")
126
+ close_count = generation.count("}") + generation.count("]")
127
+ if open_count > close_count:
128
+ patterns.append("truncation")
129
+ elif output_type == "XML":
130
+ open_tags = len(re.findall(r"<[^/!?][^>]*>", generation))
131
+ close_tags = len(re.findall(r"</[^>]+>", generation))
132
+ if open_tags > close_tags + 1:
133
+ patterns.append("truncation")
134
+ elif output_type in ("YAML", "TOML", "CSV"):
135
+ if stripped and stripped[-1] == "\\":
136
+ patterns.append("truncation")
137
+
138
+ # 空出力
139
+ if not generation.strip():
140
+ patterns.append("empty_output")
141
+
142
+ # 別フォーマットの出力(JSONを要求されたのにXMLが出てくる等)
143
+ format_indicators = {
144
+ "JSON": (r"^\s*[\{\[]", None),
145
+ "XML": (r"^\s*<", None),
146
+ "YAML": (None, None),
147
+ "TOML": (r"^\s*\[", None),
148
+ "CSV": (None, None),
149
+ }
150
+ if output_type == "JSON" and re.match(r"^\s*<", generation.strip()):
151
+ patterns.append("wrong_format")
152
+ elif output_type == "XML" and re.match(r"^\s*[\{\[]", generation.strip()):
153
+ patterns.append("wrong_format")
154
+
155
+ # CoT思考過程の混入
156
+ if re.search(r"<think>|</think>|<reasoning>|</reasoning>", generation):
157
+ patterns.append("cot_leakage")
158
+
159
+ return patterns if patterns else ["unknown"]
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # 3. Core Analysis (コア分析ロジック)
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def load_public_150(file_path: str) -> dict:
167
+ """public_150.json を読み込み、task_id → 情報 の辞書を返す"""
168
+ with open(file_path, "r", encoding="utf-8") as f:
169
+ data = json.load(f)
170
+ return {item["task_id"]: item for item in data}
171
+
172
+
173
+ def analyze_single_inference(
174
+ inference_data: list[dict],
175
+ task_info: dict,
176
+ ) -> pd.DataFrame:
177
+ """1つのinference.jsonを分析してDataFrameを返す"""
178
+ results = []
179
+ for item in inference_data:
180
+ task_id = item.get("task_id", "")
181
+ generation = item.get("generation", "")
182
+
183
+ info = task_info.get(task_id, {})
184
+ output_type = info.get("output_type", "UNKNOWN")
185
+ task_name = info.get("task_name", "UNKNOWN")
186
+
187
+ # 構文検証
188
+ validator = VALIDATORS.get(output_type)
189
+ if validator:
190
+ is_valid, error_msg = validator(generation)
191
+ else:
192
+ is_valid, error_msg = False, f"Unknown format: {output_type}"
193
+
194
+ # エラーパターン分類
195
+ if not is_valid:
196
+ error_patterns = classify_error_patterns(generation, output_type)
197
+ else:
198
+ error_patterns = []
199
+
200
+ results.append({
201
+ "task_id": task_id,
202
+ "task_name": task_name,
203
+ "output_type": output_type,
204
+ "is_valid": is_valid,
205
+ "error_msg": error_msg,
206
+ "error_patterns": ",".join(error_patterns) if error_patterns else "",
207
+ "generation_length": len(generation),
208
+ "generation_preview": generation[:200],
209
+ })
210
+
211
+ return pd.DataFrame(results)
212
+
213
+
214
+ def compute_summary(df: pd.DataFrame) -> dict:
215
+ """分析結果のサマリーを計算"""
216
+ total = len(df)
217
+ valid = df["is_valid"].sum()
218
+
219
+ summary = {
220
+ "total_tasks": total,
221
+ "parse_success": int(valid),
222
+ "parse_fail": int(total - valid),
223
+ "parse_rate": f"{valid / total * 100:.1f}%" if total > 0 else "N/A",
224
+ }
225
+
226
+ # フォーマット別
227
+ format_stats = {}
228
+ for fmt in ["JSON", "YAML", "TOML", "XML", "CSV"]:
229
+ fmt_df = df[df["output_type"] == fmt]
230
+ fmt_total = len(fmt_df)
231
+ fmt_valid = fmt_df["is_valid"].sum()
232
+ format_stats[fmt] = {
233
+ "total": fmt_total,
234
+ "success": int(fmt_valid),
235
+ "fail": int(fmt_total - fmt_valid),
236
+ "rate": f"{fmt_valid / fmt_total * 100:.1f}%" if fmt_total > 0 else "N/A",
237
+ }
238
+ summary["by_format"] = format_stats
239
+
240
+ # エラーパターン集計
241
+ all_patterns = []
242
+ for patterns_str in df[df["is_valid"] == False]["error_patterns"]:
243
+ if patterns_str:
244
+ all_patterns.extend(patterns_str.split(","))
245
+ summary["error_pattern_counts"] = dict(Counter(all_patterns).most_common())
246
+
247
+ return summary
248
+
249
+
250
+ # ---------------------------------------------------------------------------
251
+ # 4. Multi-file Comparison (複数ファイル比較)
252
+ # ---------------------------------------------------------------------------
253
+
254
+ def compare_experiments(
255
+ all_results: dict[str, pd.DataFrame],
256
+ ) -> pd.DataFrame:
257
+ """複数実験の結果を比較するDataFrameを返す"""
258
+ rows = []
259
+ for name, df in all_results.items():
260
+ total = len(df)
261
+ valid = df["is_valid"].sum()
262
+ row = {
263
+ "experiment": name,
264
+ "total": total,
265
+ "parse_success": int(valid),
266
+ "parse_rate": f"{valid / total * 100:.1f}%" if total > 0 else "N/A",
267
+ }
268
+ for fmt in ["JSON", "YAML", "TOML", "XML", "CSV"]:
269
+ fmt_df = df[df["output_type"] == fmt]
270
+ fmt_total = len(fmt_df)
271
+ fmt_valid = fmt_df["is_valid"].sum()
272
+ row[f"{fmt}_rate"] = (
273
+ f"{fmt_valid / fmt_total * 100:.1f}%"
274
+ if fmt_total > 0
275
+ else "N/A"
276
+ )
277
+ rows.append(row)
278
+ return pd.DataFrame(rows)
279
+
280
+
281
+ # ---------------------------------------------------------------------------
282
+ # 5. Gradio Interface
283
+ # ---------------------------------------------------------------------------
284
+
285
+ def process_files(public_150_file, inference_files):
286
+ """メイン処理:ファイルを受け取って分析結果を返す"""
287
+ if public_150_file is None:
288
+ return "❌ public_150.json をアップロードしてください", None, None, None, None
289
+
290
+ if not inference_files:
291
+ return "❌ inference.json を1つ以上アップロードしてくだ��い", None, None, None, None
292
+
293
+ try:
294
+ # public_150.json 読み込み
295
+ task_info = load_public_150(public_150_file.name)
296
+
297
+ all_results = {}
298
+ all_summaries = {}
299
+
300
+ for inf_file in inference_files:
301
+ filename = Path(inf_file.name).stem
302
+ with open(inf_file.name, "r", encoding="utf-8") as f:
303
+ inference_data = json.load(f)
304
+
305
+ df = analyze_single_inference(inference_data, task_info)
306
+ summary = compute_summary(df)
307
+ all_results[filename] = df
308
+ all_summaries[filename] = summary
309
+
310
+ # --- 出力1: 全体サマリーテキスト ---
311
+ summary_text = "## 📊 分析結果サマリー\n\n"
312
+ for name, s in all_summaries.items():
313
+ summary_text += f"### {name}\n"
314
+ summary_text += f"- パース成功: {s['parse_success']}/{s['total_tasks']} ({s['parse_rate']})\n"
315
+ summary_text += f"- フォーマット別:\n"
316
+ for fmt, fs in s["by_format"].items():
317
+ summary_text += f" - {fmt}: {fs['success']}/{fs['total']} ({fs['rate']})\n"
318
+ if s["error_pattern_counts"]:
319
+ summary_text += f"- エラーパターン:\n"
320
+ for pattern, count in s["error_pattern_counts"].items():
321
+ summary_text += f" - {pattern}: {count}件\n"
322
+ summary_text += "\n"
323
+
324
+ # --- 出力2: 比較テーブル ---
325
+ comparison_df = compare_experiments(all_results)
326
+
327
+ # --- 出力3: エラー詳細(最初のファイルのみ) ---
328
+ first_name = list(all_results.keys())[0]
329
+ first_df = all_results[first_name]
330
+ error_df = first_df[first_df["is_valid"] == False][
331
+ ["task_id", "task_name", "output_type", "error_msg", "error_patterns", "generation_preview"]
332
+ ]
333
+
334
+ # --- 出力4: フォーマット別パース成功率のCSV ---
335
+ format_comparison_rows = []
336
+ for name, df in all_results.items():
337
+ row = {"experiment": name}
338
+ for fmt in ["JSON", "YAML", "TOML", "XML", "CSV"]:
339
+ fmt_df = df[df["output_type"] == fmt]
340
+ fmt_total = len(fmt_df)
341
+ fmt_valid = fmt_df["is_valid"].sum()
342
+ row[fmt] = round(fmt_valid / fmt_total * 100, 1) if fmt_total > 0 else 0
343
+ format_comparison_rows.append(row)
344
+ format_df = pd.DataFrame(format_comparison_rows)
345
+
346
+ return summary_text, comparison_df, error_df, format_df, None
347
+
348
+ except Exception as e:
349
+ error_trace = traceback.format_exc()
350
+ return f"❌ エラーが発生しました:\n```\n{error_trace}\n```", None, None, None, None
351
+
352
+
353
+ # ---------------------------------------------------------------------------
354
+ # 6. Gradio App
355
+ # ---------------------------------------------------------------------------
356
+
357
+ def create_app():
358
+ with gr.Blocks(
359
+ title="StructEval-T Analyzer",
360
+ theme=gr.themes.Soft(),
361
+ ) as app:
362
+ gr.Markdown(
363
+ """
364
+ # 🔍 StructEval-T Analyzer
365
+ ### 松尾研LLM講義2025 メインコンペ用 推論結果分析ツール
366
+
367
+ `inference.json` と `public_150.json` をアップロードすることで、
368
+ モデル出力の構文的正確性(パース可能性)やエラーパターンを分析できます。
369
+
370
+ **使い方:**
371
+ 1. `public_150.json` をアップロード
372
+ 2. 1つ以上の `inference.json` をアップロード(複数ファイル対応・実験比較可能)
373
+ 3. 「分析開始」ボタンをクリック
374
+ """
375
+ )
376
+
377
+ with gr.Row():
378
+ public_file = gr.File(
379
+ label="public_150.json",
380
+ file_types=[".json"],
381
+ type="filepath",
382
+ )
383
+ inference_files = gr.File(
384
+ label="inference.json(複数可)",
385
+ file_types=[".json"],
386
+ file_count="multiple",
387
+ type="filepath",
388
+ )
389
+
390
+ analyze_btn = gr.Button("🔬 分析開始", variant="primary", size="lg")
391
+
392
+ with gr.Tabs():
393
+ with gr.Tab("📊 サマリー"):
394
+ summary_output = gr.Markdown()
395
+
396
+ with gr.Tab("📈 実験比較"):
397
+ comparison_table = gr.Dataframe(
398
+ label="実験間のパース成功率比較",
399
+ interactive=False,
400
+ )
401
+
402
+ with gr.Tab("❌ エラー詳細"):
403
+ gr.Markdown("*最初にアップロードされたファイルのエラー一覧を表示*")
404
+ error_table = gr.Dataframe(
405
+ label="パース失敗タスク一覧",
406
+ interactive=False,
407
+ wrap=True,
408
+ )
409
+
410
+ with gr.Tab("📉 フォーマット別"):
411
+ format_table = gr.Dataframe(
412
+ label="フォーマット別パース成功率(%)",
413
+ interactive=False,
414
+ )
415
+
416
+ analyze_btn.click(
417
+ fn=process_files,
418
+ inputs=[public_file, inference_files],
419
+ outputs=[summary_output, comparison_table, error_table, format_table, gr.State()],
420
+ )
421
+
422
+ gr.Markdown(
423
+ """
424
+ ---
425
+ **注意:** このツールは構文的な正確性(パース可能かどうか)のみを検証します。
426
+ 運営側の採点基準である `raw_output_metric`(特定キーの存在チェック等)は
427
+ `public_150.json` から削除されているため、完全なスコア再現はできません。
428
+
429
+ **エラーパターンの凡例:**
430
+ - `markdown_block`: マークダウンコードブロック(\\`\\`\\`json 等)の混入
431
+ - `natural_language_prefix`: 先頭に自然言語("Here is..."等)が混入
432
+ - `natural_language_suffix`: 末尾に自然言語("Note:"等)が混入
433
+ - `truncation`: 出力の途切れ(閉じ括弧・タグの欠落)
434
+ - `empty_output`: 空の出力
435
+ - `wrong_format`: 要求と異なるフォーマットの出力
436
+ - `cot_leakage`: 思考過程(\\<think\\>等)の混入
437
+ - `unknown`: 上記に該当しない構文エラー
438
+ """
439
+ )
440
+
441
+ return app
442
+
443
+
444
+ if __name__ == "__main__":
445
+ app = create_app()
446
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas
3
+ pyyaml