File size: 14,862 Bytes
74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd 74648a6 66743bd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
import streamlit as st
import pandas as pd
from sentence_transformers import SentenceTransformer
import torch
import torch.nn.functional as F
import io
import json
# --- 1. デフォルトの階層カテゴリ定義 ---
# ユーザーが編集可能なデフォルトの定義
DEFAULT_HIERARCHICAL_CATEGORIES = {
"UI/UX (使いやすさ)": {
"description": "デザインや操作性など、サービスの使いやすさに関する意見。",
"sub_categories": {
"デザインが良い・悪い": "見た目のデザイン、レイアウト、色使いなどに関する意見。",
"操作が分かりにくい": "ボタンの場所が分からない、設定方法が難しいなど、操作方法に関する意見。",
"動作が重い・遅い": "ページの読み込みが遅い、アプリがフリーズするなど、システムの応答速度に関する意見。",
"その他": "上記以外の使いやすさに関する意見。"
}
},
"機能": {
"description": "サービスが提供する個別の機能に関する意見。",
"sub_categories": {
"機能への要望": "「〇〇という機能が欲しい」といった、新しい機能の追加を求める意見。",
"機能が便利・役に立った": "特定の機能が使いやすかった、役に立ったというポジティブな意見。",
"不具合・バグ報告": "「ボタンが押せない」「エラーが出る」など、機能が正常に動作しない問題の報告。",
"その他": "上記以外の機能に関する意見。"
}
},
"サポート": {
"description": "問い合わせ対応など、カスタマーサポートに関する意見。",
"sub_categories": {
"対応が良かった・悪かった": "サポート担当者の対応が丁寧だった、あるいは不親切だったという意見。",
"返信が来ない・遅い": "問い合わせても返信がない、または返信が非常に遅いことへの不満。",
"その他": "上記以外のサポートに関する意見。"
}
},
"価格・料金": {
"description": "サービスの料金プランや価格設定に関する意見。",
"sub_categories": {
"価格が高い・安い": "価格設定が内容に見合って高い、あるいは安いと感じる意見。",
"料金体系が分かりにくい": "料金プランが複雑で理解しにくいという意見。",
"その他": "上記以外の価格に関する意見。"
}
},
"その他・要望": {
"description": "上記のいずれにも当てはまらない意見や、サービス全体への要望。",
"sub_categories": {
"感謝・応援": "サービス全体への感謝や、応援するポジティブな意見。",
"アイデア提案": "具体的な改善案や、新しいサービスのアイデアに関する提案。",
"その他": "上記のいずれにも明確に当てはまらない、その他の意見。"
}
}
}
# --- 2. AIモデルの読み込み ---
@st.cache_resource
def load_model():
"""SentenceTransformerモデルをHugging Face Hubから直接ロードする"""
try:
model = SentenceTransformer('cl-nagoya/ruri-v3-310m')
st.success("✅ AIモデルの読み込みに成功しました。")
return model
except Exception as e:
st.error(f"モデルの読み込み中にエラーが発生しました: {e}")
st.stop()
# --- 3. 分類ロジック ---
# 3-1. 単一カテゴリ分類(既存のロジック)
def classify_text(text, model, definitions_dict):
"""与えられたテキストを、定義辞書に基づいて最も類似度の高いカテゴリに分類する"""
if not text or not isinstance(text, str) or not text.strip():
return "テキストが空です"
try:
category_definitions = list(definitions_dict.values())
texts_to_encode = category_definitions + [text]
embeddings = model.encode(texts_to_encode, convert_to_tensor=True)
text_embedding = embeddings[-1]
definition_embeddings = embeddings[:-1]
similarities = F.cosine_similarity(text_embedding, definition_embeddings)
most_similar_index = torch.argmax(similarities).item()
result_category = list(definitions_dict.keys())[most_similar_index]
return result_category
except Exception as e:
st.error(f"分類中にエラーが発生しました: {e}")
return "分類エラー"
# 3-2. 階層カテゴリ分類(新規追加)
def classify_subcategory(comment, main_category, model, hierarchical_defs, threshold=0.6):
"""埋め込みモデルの類似度計算によってサブカテゴリを分類する"""
if main_category not in hierarchical_defs or "sub_categories" not in hierarchical_defs[main_category]:
return "サブカテゴリなし"
sub_category_dict = hierarchical_defs[main_category]["sub_categories"]
if not sub_category_dict:
return "サブカテゴリなし"
definitions = [f"{name}: {desc}" for name, desc in sub_category_dict.items()]
embeddings = model.encode([comment] + definitions, convert_to_tensor=True)
comment_embedding = embeddings[0]
definition_embeddings = embeddings[1:]
similarities = F.cosine_similarity(comment_embedding, definition_embeddings)
best_match_index = torch.argmax(similarities).item()
best_match_score = similarities[best_match_index].item()
if best_match_score < threshold:
return "その他"
sub_category_names = list(sub_category_dict.keys())
return sub_category_names[best_match_index]
def analyze_hierarchically(comment, model, hierarchical_defs, sentiment_labels):
"""センチメント分析、大カテゴリ分類、サブカテゴリ分類を順番に行う"""
if not comment or not isinstance(comment, str) or not comment.strip():
return {"sentiment": "エラー", "category": "コメントが空", "sub_category": ""}
try:
# 1. センチメント分析
sentiment_texts = sentiment_labels + [comment]
sentiment_embeddings = model.encode(sentiment_texts, convert_to_tensor=True)
sentiment_similarities = F.cosine_similarity(sentiment_embeddings[-1], sentiment_embeddings[:-1])
sentiment = "ポジティブ" if torch.argmax(sentiment_similarities) == 0 else "ネガティブ"
# 2. 大カテゴリ分類
category_descriptions = [v["description"] for v in hierarchical_defs.values()]
category_texts = category_descriptions + [comment]
category_embeddings = model.encode(category_texts, convert_to_tensor=True)
category_similarities = F.cosine_similarity(category_embeddings[-1], category_embeddings[:-1])
main_category = list(hierarchical_defs.keys())[torch.argmax(category_similarities)]
# 3. サブカテゴリ分類
sub_category = classify_subcategory(comment, main_category, model, hierarchical_defs, threshold=0.6)
return {"sentiment": sentiment, "category": main_category, "sub_category": sub_category}
except Exception as e:
st.error(f"階層分析中にエラーが発生しました: {e}")
return {"sentiment": "分析エラー", "category": "分析エラー", "sub_category": str(e)}
# --- 4. Streamlit アプリケーションのUIとメイン処理 ---
st.set_page_config(layout="wide")
st.title("📝 テキスト分類ツール (単一/階層)")
st.markdown("アップロードしたファイルのテキストを、定義に基づいて分類します。")
# --- モデル読み込み ---
with st.spinner('AIモデルを読み込んでいます...'):
model = load_model()
# --- 分析タイプ選択 ---
analysis_type = st.radio(
"分析の種類を選択してください",
('単一カテゴリ分析', '階層カテゴリ分析'),
horizontal=True,
help="「単一カテゴリ分析」は複数の観点で分類します。「階層カテゴリ分析」は大カテゴリ→サブカテゴリの2段階で詳細に分類します。"
)
st.header("Step 1: ファイルをアップロード")
uploaded_file = st.file_uploader("分類したいテキストデータが含まれるファイルを選択してください", type=["csv", "xlsx"])
# --- 列選択 ---
selected_column = None
if uploaded_file is not None:
try:
# ファイルをメモリに読み込んでから列名を取得
uploaded_file.seek(0)
if uploaded_file.name.endswith('.csv'):
df_peek = pd.read_csv(uploaded_file, nrows=0)
else:
df_peek = pd.read_excel(uploaded_file, nrows=0)
column_options = df_peek.columns.tolist()
selected_column = st.selectbox("分類したいテキストが含まれている列を選択してください", options=column_options)
except Exception as e:
st.error(f"ファイルの列読み込みに失敗しました: {e}")
st.header("Step 2: カテゴリを定義")
# --- 分析タイプに応じたUIの表示 ---
if analysis_type == '単一カテゴリ分析':
st.markdown("分類したい観点ごとにカテゴリ定義を追加・編集してください。(最大5つまで)")
if 'definition_sets' not in st.session_state:
st.session_state.definition_sets = [
"""ポジティブ: 肯定的、好意的、賞賛、感謝の意見
ネガティブ: 否定的、批判的、不満、改善要望の意見
質問: 何かに対する疑問や問いかけ"""
]
def add_definition_set():
if len(st.session_state.definition_sets) < 5:
st.session_state.definition_sets.append("カテゴリ名1: 説明1\nカテゴリ名2: 説明2")
def remove_definition_set(index):
if len(st.session_state.definition_sets) > 1:
st.session_state.definition_sets.pop(index)
for i, def_text in enumerate(st.session_state.definition_sets):
st.subheader(f"分類セット {i+1}")
st.session_state.definition_sets[i] = st.text_area(
f"カテゴリ定義 {i+1}", value=def_text, height=120, key=f"def_area_{i}"
)
if len(st.session_state.definition_sets) > 1:
st.button(f"分類セット {i+1} を削除", key=f"remove_btn_{i}", on_click=remove_definition_set, args=(i,))
if len(st.session_state.definition_sets) < 5:
st.button("+ カテゴリ定義を追加", on_click=add_definition_set)
else: # 階層カテゴリ分析
st.markdown("大カテゴリとサブカテゴリの関係をJSON形式で定義してください。")
default_defs_str = json.dumps(DEFAULT_HIERARCHICAL_CATEGORIES, indent=4, ensure_ascii=False)
hierarchical_defs_text = st.text_area(
"階層カテゴリ定義(JSON形式)",
value=default_defs_str,
height=500
)
st.header("Step 3: 分類を実行")
if st.button("分類を実行する", type="primary"):
if uploaded_file is None or selected_column is None:
st.warning("Step 1でファイルと列を正しく設定してください。")
else:
# --- 分類処理の実行 ---
try:
uploaded_file.seek(0)
if uploaded_file.name.endswith('.csv'):
df = pd.read_csv(uploaded_file)
else:
df = pd.read_excel(uploaded_file)
progress_bar = st.progress(0, text="分類処理を開始します...")
total_rows = len(df)
if analysis_type == '単一カテゴリ分析':
# --- 単一カテゴリ分析の処理 ---
definition_dicts = []
for def_text in st.session_state.definition_sets:
temp_dict = {parts[0].strip(): parts[1].strip() for line in def_text.strip().split('\n') if ':' in line and (parts := line.split(':', 1))}
if temp_dict:
definition_dicts.append(temp_dict)
if not definition_dicts:
st.error("有効なカテゴリ定義がありません。")
st.stop()
for i, row in df.iterrows():
text_to_classify = str(row[selected_column]) if pd.notna(row[selected_column]) else ""
for j, def_dict in enumerate(definition_dicts):
result_col_name = f"分類結果_{j+1}"
result = classify_text(text_to_classify, model, def_dict)
df.loc[i, result_col_name] = result
progress_bar.progress((i + 1) / total_rows, text=f"{i+1}/{total_rows} 件処理完了")
else:
# --- 階層カテゴリ分析の処理 ---
hierarchical_defs = json.loads(hierarchical_defs_text)
SENTIMENT_LABELS = ["ポジティブな意見", "ネガティブな意見"]
for i, row in df.iterrows():
comment = str(row[selected_column]) if pd.notna(row[selected_column]) else ""
result = analyze_hierarchically(comment, model, hierarchical_defs, SENTIMENT_LABELS)
df.loc[i, 'センチメント'] = result['sentiment']
df.loc[i, '大カテゴリ'] = result['category']
df.loc[i, 'サブカテゴリ'] = result['sub_category']
progress_bar.progress((i + 1) / total_rows, text=f"{i+1}/{total_rows} 件処理完了")
st.success("🎉 分類が完了しました!")
st.subheader("分類結果プレビュー")
st.dataframe(df.head())
# --- ダウンロード処理 ---
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='classified_data')
processed_data = output.getvalue()
base_filename = uploaded_file.name.rsplit('.', 1)[0]
st.download_button(
label="📁 分類済みExcelをダウンロード",
data=processed_data,
file_name=f"classified_{base_filename}.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
except json.JSONDecodeError:
st.error("JSON形式の階層カテゴリ定義が正しくありません。構文を確認してください。")
except Exception as e:
st.error(f"処理中に予期せぬエラーが発生しました: {e}")
|