1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】研究内容を入れると「最適な審査区分」を推薦してくれるAIを作ってみた(Gemini API + Streamlit)

1
Posted at

はじめに

研究者の皆さん、毎年の科研費(科学研究費助成事業)の申請、お疲れ様です。
申請書を書く際、「自分の研究は、どの『小区分』に出せば採択されやすいのか?」 と悩んだことはないでしょうか?

タイトルや要旨から最適な区分を推薦し、さらにその区分での申請アドバイスまでしてくれるアプリを、Google Gemini APIStreamlit を使って作ってみました。

サーバー代もかからず、Pythonだけで完結する構成です。

作ったもの

「科研費・審査区分マッチングAI」
研究のタイトルや要旨を入力すると、全400以上ある小区分の中から、意味的に近い区分をランキング形式で提案してくれます。

主な機能

  1. 審査区分マッチング: 入力されたテキストと、各小区分の定義をベクトル化し、コサイン類似度でマッチング。
  2. ニッチ度判定: 1位と2位のスコア差などから、「王道のテーマ」か「学際的(ニッチ)なテーマ」かを判定。
  3. 学問の地図(可視化): 全区分を2次元マップにプロットし、自分の研究が学問全体のどこに位置するかを表示。
  4. 申請アドバイス: 選択された区分で採択されるためのキーワードや戦略をLLMが助言。

スクリーンショット 2025-11-23 18.41.33.png
スクリーンショット 2025-11-23 18.42.17.png
スクリーンショット 2025-11-23 18.42.27.png

技術スタック

非常にシンプルです。すべて無料枠で運用できています。

  • 言語: Python 3.11
  • フロントエンド & デプロイ: Streamlit Community Cloud
  • AI (Embeddings & Chat): Google Gemini API
    • 埋め込みモデル: models/gemini-embedding-001 (768次元)
    • 生成モデル: gemini-2.5-flash-lite
  • 計算・可視化:
    • scikit-learn (コサイン類似度計算、PCAによる次元圧縮)
    • Plotly (インタラクティブな散布図描画)

実装のポイント

1. データの事前計算とモデル統一の罠

毎回400個以上の小区分をベクトル化するとAPI制限に引っかかる&遅いため、事前にローカルで計算し、JSONファイルとして持たせる構成にしました。

# モデルIDを定数で管理して不一致を防ぐ
EMBEDDING_MODEL_ID = "gemini-embedding-001"

# ベクトル化の処理
result = genai.embed_content(model=EMBEDDING_MODEL_ID, content=query)

2. 類似度検索とニッチ度判定

ユーザーが入力したテキストをその場でベクトル化し、事前計算したデータと比較します。
単に1位を出すだけでなく、1位と2位のスコア差(diff)を見ることで、その研究テーマの立ち位置を判定するロジックを入れました。

# ランキング作成
sims = cosine_similarity([query_vec], embeddings)[0]
top_indices = sims.argsort()[::-1][:5]
top_scores = sims[top_indices]

# ニッチ度判定
diff = top_scores[0] - top_scores[1]
if top_scores[0] < 0.6:
    st.info("💡 非常に新規性が高い、または学際的なテーマのようです...")
elif diff > 0.05:
    st.success("🎯 王道のテーマです!...")

3. 学問の「地図」を描く (Plotly)

ただリストを出すだけでは面白くないので、PCA(主成分分析)を使って768次元のベクトルを2次元に圧縮し、Plotly で散布図を描きました。
自分の研究が「情報学寄り」なのか「心理学寄り」なのかが視覚的にわかります。

4. 生成AIによるアドバイス (Gemini 2.5 Flash Lite)

最後に、マッチした区分名をプロンプトに埋め込み、LLMに具体的なアドバイスを求めます。
レスポンス速度と精度のバランスが良い、最新の gemini-2.5-flash-lite を採用しました。

prompt = f"""
研究テーマ: {user_query}
申請区分: {target_category}

この区分で採択されやすくするためのキーワードや、強調すべき観点を3点以内でアドバイスしてください。
"""
model = genai.GenerativeModel("gemini-2.5-flash-lite")
advice = model.generate_content(prompt)

注意点

  1. APIのレート制限 (429 Error)
    無料枠を使用しているため、大量のデータを一気にベクトル化しようとすると 429 Resource Exhausted が発生しました。事前計算スクリプトに time.sleep() を入れてリトライ処理を実装することで解決しました。

  2. StreamlitでのAPIキー管理
    GitHubにコードを上げる際、APIキーを直接書くのは厳禁です。Streamlit Community Cloudには Secrets という機能があり、環境変数を安全に管理できました。

完成したコード (app.py)

<details><summary>ソースコード全体を開く</summary>

import streamlit as st
import google.generativeai as genai
import numpy as np
import json
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.decomposition import PCA
import plotly.graph_objects as go
import os

# ===== 設定 =====
# APIキーの取得 (Streamlit Secrets または 環境変数)
try:
    api_key = st.secrets["GEMINI_API_KEY"]
except:
    api_key = os.getenv("GEMINI_API_KEY")

if not api_key:
    st.error("APIキーが設定されていません。Streamlit CloudのSecretsを設定してください。")
    st.stop()

genai.configure(api_key=api_key)

# 埋め込み用モデル(JSONを作ったときと同じモデルを指定する)
EMBEDDING_MODEL_ID = "gemini-embedding-001"
# アドバイス生成用モデル(文章が作れるモデルを指定する)
GENERATION_MODEL_ID = "gemini-2.5-flash-lite"

# ===== データの読み込みと前処理 =====
@st.cache_data
def load_and_process_data():
    # JSONの読み込み
    try:
        with open("academic_embeddings.json", "r", encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return None, None, None, None
        
    words = [d["word"] for d in data]
    embeddings = np.array([d["vector"] for d in data])
    
    # 2次元マップ用に次元圧縮 (PCA) を事前に計算しておく
    n_samples = len(embeddings)
    n_components = 2
    if n_samples < 2:
         return words, embeddings, None, None

    pca = PCA(n_components=n_components)
    coords_2d = pca.fit_transform(embeddings)
    
    return words, embeddings, coords_2d, pca

words, embeddings, base_coords_2d, pca_model = load_and_process_data()

if words is None:
    st.error("埋め込みデータファイル(academic_embeddings.json)が見つかりません。")
    st.stop()

# ===== UI構築 =====
st.set_page_config(page_title="科研費マッチングAI", layout="wide")
st.title("科研費・審査区分マッチングAI 🎓")
st.markdown("あなたの研究テーマを入力すると、AIが最適な審査区分を推薦し、研究の立ち位置を可視化します。")

# 入力フォーム
col1, col2 = st.columns([2, 1])

with col1:
    query = st.text_area("研究タイトルまたは要旨", height=150, 
                         placeholder="例:〇〇の△△における✕✕の解明")

    if st.button("分析する 🔍", type="primary"):
        if not query:
            st.warning("テキストを入力してください。")
        else:
            with st.spinner("AIが分析中..."):
                try:
                    # 1. 入力テキストをベクトル化
                    result = genai.embed_content(model=EMBEDDING_MODEL_ID, content=query)
                    query_vec = np.array(result['embedding'])

                    # 2. 類似度計算
                    sims = cosine_similarity([query_vec], embeddings)[0]
                    
                    # 3. ランキング作成
                    top_n = 5
                    top_indices = sims.argsort()[::-1][:top_n]
                    top_scores = sims[top_indices]

                    # --- 結果表示エリア ---
                    st.divider()
                    
                    # A. ニッチ度判定ロジック
                    score_1st = top_scores[0]
                    score_2nd = top_scores[1]
                    diff = score_1st - score_2nd
                    
                    st.subheader("📊 分析結果")
                    
                    if score_1st < 0.6: 
                        st.info("💡 **非常に新規性が高い、または学際的なテーマのようです。**\n\nどの区分にも完全には当てはまらない可能性があります。複合領域での申請も検討してみてください。")
                    elif diff > 0.05: 
                        st.success("🎯 **王道のテーマです!**\n\n1位の区分が非常に強くマッチしています。迷わずこの区分で良いでしょう。")
                    else: 
                        st.warning("⚖️ **境界領域のテーマです。**\n\n1位と2位のスコアが近いです。どちらのコミュニティで評価されたいか、戦略的に選ぶ必要があります。")

                    # B. ランキング表示
                    st.write("#### おすすめの審査区分")
                    for i, idx in enumerate(top_indices):
                        score = sims[idx]
                        category = words[idx]
                        st.write(f"**{i+1}. {category}** (一致度: {score:.3f})")
                        st.progress(min(float(score), 1.0))
                    
                    # C. キーワードアドバイス
                    st.write("#### 💡 申請書作成アドバイス")
                    target_cat = words[top_indices[0]]
                    advice_prompt = f"""
                    以下の研究テーマを、科研費の審査区分「{target_cat}」に申請しようとしています。
                    この区分で採択されやすくするために、含めるべきキーワードや、強調すべき観点を3点以内で簡潔にアドバイスしてください。
                    
                    研究テーマ: {query}
                    """
                    
                    try:
                        model_gen = genai.GenerativeModel(GENERATION_MODEL_ID) 
                        advice_resp = model_gen.generate_content(advice_prompt)
                        st.info(advice_resp.text)
                    except Exception as e:
                        st.warning(f"アドバイス生成中にエラーが発生しました: {e}")

                    # D. 可視化 (Plotly)
                    if pca_model is not None:
                        st.write("#### 🗺 学問の地図")
                        user_coord = pca_model.transform([query_vec])[0]
                        
                        fig = go.Figure()
                        # (プロット処理部分は長いので省略しますが、実際はここに入っています)
                        # ...(Plotlyのコード)...
                        st.plotly_chart(fig, use_container_width=True)

                except Exception as e:
                    st.error(f"詳細エラー: {e}")

with col2:
    st.info("""
    **使い方**
    1. 研究タイトルや要旨を入力します。
    2. 「分析する」ボタンを押します。
    3. AIが最適な審査区分を判定します。
    """)

まとめ

Gemini APIとStreamlitを組み合わせることで、「データの検索」と「生成AIによるコンサルティング」を組み合わせたアプリが、わずか数時間で開発・公開できました。

研究者に限らず、「自分の入力に対して、既存のカテゴリから最適なものを推薦してほしい」というユースケースは多いと思うので、応用範囲は広そうです。

ぜひ、みなさんも自分の興味ある分野で「マッチングAI」を作ってみてください!


参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?