見出し画像

ChatGPTとStreamlitを使ってarXiv論文を要約するWebアプリを開発しよう!

アプリの目的と概要

この記事では、StreamlitとChatGPTを使って、論文要約アプリを作成する方法を紹介します。 このアプリでは、キーワードと指定された期間を入力するとその期間のarXiv論文を検索できます。検索結果の一覧から選択した論文に対して、ChatGPTを使ってabstractの要約を生成します。これで、日本語で簡単に論文の内容を理解できるようになります。追加機能として、全文要約や論文の内容に質問できるような機能もできたらなと思っていますが今回は未実装です。

アプリの開発には、StreamlitとChatGPTAPIを使います。特にChatGPTAPIでは6月に新しく追加されたFunctionを使ってみました。ユーザーインターフェースをStreamlitで作成し、arXiv APIで論文の検索を行います。

2. 開発準備

今回は、前半の機能面を作る部分ではGoogleColaboratoryを使って、後半のアプリにする部分はローカル環境のWindowsを使いました。(最後までGoogleColaboratoryでも可能ですがいろいろアプリ作るならローカルの方が手軽かなと思います。)

必要なライブラリのインストール

必要なライブラリをインストールします。以下のコマンドをコマンドラインまたはターミナルで実行してください。

pip install streamlit requests pytz xmltodict openai

OpenAI APIキーの取得

論文要約にはOpenAIのChatGPT APIを使用します。APIを使用するには、OpenAIのアカウントが必要です。また、APIキーを取得する必要があります。以下の手順に従ってAPIキーを取得してください。詳細は他のサイトにも詳しくあるので、そちらを参照してください。

  1. OpenAIのウェブサイトにアクセスします。

  2. アカウントを作成し、ログインします。

  3. ダッシュボードに移動し、APIキーを生成します。

APIキーは他人に知られないよう注意して管理してください。

3. アプリの開発

arXiv APIから論文情報を取得する関数の実装

このセクションでは、arXiv APIから論文情報を取得するための関数を実装します。arXiv APIは、論文のタイトル、要約、投稿日時などの情報を提供します。

具体的には、以下の手順で関数を実装します。

  1. arXiv APIのエンドポイントを設定。

  2. XMLデータを解析し、必要な情報を抽出するための関数を作成。

  3. 検索クエリ、開始日、終了日を指定して、arXiv APIにリクエストを送信し、論文情報を取得。

これにより、指定した条件に合致する論文情報を取得することができます。

今回は期間指定をしない場合、直近1週間の論文を検索するようにしています。

ポイントは、期間指定の検索は、

'search_query': f'{query} AND submittedDate:[{start_date} TO {end_date}235959]'

のようにquery部分に入れることです。

import datetime
import requests
import datetime
import pytz
import xml.etree.ElementTree as ET
​
​
def parse_xml(xml_data):
    # XMLデータを解析してタイトル、要約、投稿日時を抽出する処理を実装する
    root = ET.fromstring(xml_data)
    papers = []
    for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):
        title = entry.find('{http://www.w3.org/2005/Atom}title').text
        summary = entry.find('{http://www.w3.org/2005/Atom}summary').text
        published = entry.find('{http://www.w3.org/2005/Atom}published').text
        papers.append((title, summary, published))
    return papers
​
​
def search_arxiv_papers(query, start_date=None, end_date=None):
    # arXiv APIのエンドポイントとパラメータを指定
    base_url = 'http://export.arxiv.org/api/query'
​
    # クエリパラメータの設定
    params = {
        'search_query': f'{query} AND submittedDate:[{start_date} TO {end_date}235959]',
        'max_results': 5,  # 取得する論文の最大数
        'sortBy': 'relevance',  # 関連性に基づいてソート
        'sortOrder': 'descending',  # 降順にソート
    }
​
    # APIリクエストを送信してレスポンスを取得
    response = requests.get(base_url, params=params)
​
    if response.status_code == 200:
        # レスポンスのXMLデータを解析してタイトルと要約を取得
        xml_data = response.content
        papers = parse_xml(xml_data)
        return papers
    else:
        print('Failed to fetch papers from arXiv API.')
        return []
​
​
# キーワードを入力
query = input('検索キーワードを入力してください: ')
​
# 検索期間の指定(任意)
start_date_input = input('開始日をYYYYMMDD形式で入力してください(未入力の場合は直近1週間になります): ')
end_date_input = input('終了日をYYYYMMDD形式で入力してください(未入力の場合は現在日時になります): ')
​
# JST(日本時間)のタイムゾーンを設定
jst = pytz.timezone('Asia/Tokyo')
​
# 現在の日付を取得(JST)
now = datetime.datetime.now(jst)
​
# 1週間前の日付を計算
one_week_ago = now - datetime.timedelta(weeks=1)
​
# YYYYMMDD形式の文字列をdatetimeオブジェクトに変換
start_date = start_date_input if start_date_input else one_week_ago.strftime('%Y%m%d')
end_date = end_date_input if end_date_input else now.strftime('%Y%m%d')
​
# arXiv APIを使用して論文を検索
papers = search_arxiv_papers(query, start_date, end_date)
​
# タイトルと要約を表示
for i, (title, summary, published) in enumerate(papers, start=1):
    print(f'論文 {i}')
    print('--------')
    print('タイトル:', title)
    print('投稿日時:', published)
    print('要約:')
    print(summary)
    print('--------\n')
​


ChatGPTを使用して要約を生成する関数の実装

このセクションでは、ChatGPT APIを使用して要約を生成するための関数を実装します。

具体的には、以下の手順で関数を実装します。

  1. ChatGPT APIに接続するための設定。APIキーは自分のキーを入力してください。

  2. 要約を生成するための関数を作成。

  3. 入力テキストをChatGPT APIに送信し、要約を取得。

今回は新しく追加されたfunctionsを使ってみました。これを使うことで、出力形式をかなり精度よく指定できるようになりました。

import openai
openai.api_key = "APIキー"
functions = [
{
    "name": "output_format",
    "description": "あなたは研究者です。以下の論文の要約文章を読んで、以下の4つの問いに日本語で答えてください。",
    "parameters": {
    "type": "object",
    "properties": {
        "short_summary": {
        "type": "string",
        "description": "この研究を一言で表すと",
        },
        "problem": {
        "type": "string",
        "description": "既存研究の問題点や課題は?",
        },
     "how": {
        "type": "string",
        "description": "この研究ではどのようなアプローチを行ったのか?",
        },
        "resultd": {
        "type": "string",
        "description": "どのような結果や結論が得られたか",
        },
    },
    "required": ["short_summary","problem", "how", "result"],
    },
}
]
content = '''あなたは研究者です。以下の論文の要約文章を読んで、以下の問いに日本語で答えてください。
- この研究を一言で表現すると?
- 既存研究の問題点や課題は?
- この研究ではどのようなアプローチを行ったのか?
- どのような結果や結論が得られたか
'''
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    functions=functions,
    messages=[
        # {"role":"system", "content":content},#精度はこれがある方がよい
        {"role": "user", "content": summary},
    ],
)
output_msg = response["choices"][0]["message"]["function_call"]["arguments"]
import json
# 文字列を辞書に変換
output_dict = json.loads(output_msg)
​
# 各アイテムの値を表示
for key, value in output_dict.items():
    print(f"{key}: {value}")

このような出力になります。


ちなみに後でアプリにしたときや、そのた開発過程で毎回API呼び出すのはコストがかかるので、以下のようにresponseを保存して使えるようにしておくのがおすすめです。

import pickle
​
# responseオブジェクトを保存するファイルパス
file_path = "response.pkl"# responseオブジェクトをpickle形式で保存
with open(file_path, "wb") as file:
    pickle.dump(response, file)
# 保存したresponseオブジェクトを読み込むファイルパス
file_path = "response.pkl"# pickleファイルからresponseオブジェクトを読み込む
with open(file_path, "rb") as file:
    response2 = pickle.load(file)

4. Streamlitを使用した論文要約アプリの開発

このセクションでは、Streamlitを使用して論文要約アプリを開発します。Streamlitは、PythonのWebアプリケーションフレームワークであり、簡単にインタラクティブなアプリケーションを作成することができます。

具体的には、以下の手順でアプリを開発します。

  1. 必要なパッケージのインポートと設定。

  2. arXiv APIから論文情報を取得する関数を実装。

  3. ChatGPTを使用して要約を生成する関数を実装。

  4. Streamlitアプリのメイン関数を作成。

  5. APIキーの入力と設定。

  6. キーワードと検索期間の入力を受け付け、論文検索。

  7. 検索結果を表示。

  8. 論文に関する質問形式の要約を生成。

  9. 要約を表示。

ローカル作業環境で、app.pyを作成し、以下を記入します。

import streamlit as st
import requests
import json
import datetime
import pytz
import xml.etree.ElementTree as ET
import openai
​
​
# arXiv APIのエンドポイント
arxiv_api_url = 'http://export.arxiv.org/api/query'
​
​
def parse_xml(xml_data):
    # XMLデータを解析してタイトル、要約、投稿日時を抽出する
    root = ET.fromstring(xml_data)
    papers = []
    for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):
        title = entry.find('{http://www.w3.org/2005/Atom}title').text
        summary = entry.find('{http://www.w3.org/2005/Atom}summary').text
        published = entry.find('{http://www.w3.org/2005/Atom}published').text
        papers.append({'title':title, 'summary':summary, 'published':published})
    return papers
​
​
def search_arxiv_papers(query, start_date=None, end_date=None):
    # arXiv APIのエンドポイントとパラメータを指定
    base_url = 'http://export.arxiv.org/api/query'
​
    # クエリパラメータの設定
    params = {
        'search_query': f'{query} AND submittedDate:[{start_date} TO {end_date}235959]',
        'max_results': 5,  # 取得する論文の最大数
        'sortBy': 'relevance',  # 関連性に基づいてソート
        'sortOrder': 'descending',  # 降順にソート
    }
​
    # APIリクエストを送信してレスポンスを取得
    response = requests.get(base_url, params=params)
​
    if response.status_code == 200:
        # レスポンスのXMLデータを解析してタイトルと要約を取得
        xml_data = response.content
        papers = parse_xml(xml_data)
        return papers
    else:
        print('Failed to fetch papers from arXiv API.')
        return []
​
​
def generate_summary(text):
    # ChatGPT APIを使用して要約を生成
    functions = [
    {
        "name": "output_format",
        "description": "あなたは研究者です。以下の論文の要約文章を読んで、以下の4つの問いに日本語で答えてください。",
        "parameters": {
        "type": "object",
        "properties": {
            "short_summary": {
            "type": "string",
            "description": "この研究を一言で表すと",
            },
            "problem": {
            "type": "string",
            "description": "既存研究の問題点や課題は?",
            },
            "how": {
            "type": "string",
            "description": "この研究ではどのようなアプローチを行ったのか?",
            },
            "result": {
            "type": "string",
            "description": "どのような結果や結論が得られたか",
            },
        },
        "required": ["short_summary","problem", "how", "result"],
        },
    }
    ]
    # content = '''あなたは研究者です。以下の論文の要約文章を読んで、以下の問いに日本語で答えてください。
    # - この研究を一言で表現すると?
    # - 既存研究の問題点や課題は?
    # - この研究ではどのようなアプローチを行ったのか?
    # - どのような結果や結論が得られたか
    # '''
    if st.session_state["api_key"] == "":
        import pickle
​
        # 保存したresponseオブジェクトを読み込むファイルパス
        file_path = "../response.pkl"
​
        # pickleファイルからresponseオブジェクトを読み込む
        with open(file_path, "rb") as file:
            response = pickle.load(file)
    else:
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            functions=functions,
            messages=[
                # {"role":"system", "content":content},#精度はこれがある方がよい
                {"role": "user", "content": text},
            ],
        )
        
​
    
    output_msg = response["choices"][0]["message"]["function_call"]["arguments"]
    output_dict = json.loads(output_msg)
    return output_dict
    
        
def set_api():
    openai.api_key = st.session_state["api_key"]
​
​
​
​
def main():
    st.title('論文要約アプリ')
​
    #API入力
    api_key = st.text_input("OpenAI APIキーを入力してください", on_change=set_api, key='api_key')
​
    # キーワードの入力
    query = st.text_input('キーワードを入力してください')
    # 検索期間の指定
    start_date_input = st.text_input('開始日をYYYYMMDD形式で入力してください(未入力の場合は直近1週間になります)')
    end_date_input = st.text_input('終了日をYYYYMMDD形式で入力してください(未入力の場合は現在日時になります)')
    if "papers" not in st.session_state:
        st.session_state['papers'] = None
    
    
​
    # 検索ボタンが押された場合の処理
    if st.button('検索', key='search_button'):
        if query:
            
            # JST(日本時間)のタイムゾーンを設定
            jst = pytz.timezone('Asia/Tokyo')
​
            # 現在の日付を取得(JST)
            now = datetime.datetime.now(jst)
​
            # 1週間前の日付を計算
            one_week_ago = now - datetime.timedelta(weeks=1)
​
            # YYYYMMDD形式の文字列をdatetimeオブジェクトに変換
            start_date = start_date_input if start_date_input else one_week_ago.strftime('%Y%m%d')
            end_date = end_date_input if end_date_input else now.strftime('%Y%m%d')
            st.write(start_date)
            # arXiv APIを使用して論文を検索
            papers = search_arxiv_papers(query, start_date, end_date)
            if papers:
                st.session_state['papers']= papers
            else:
                st.warning('該当する論文が見つかりませんでした。')
    
    # 論文の検索結果を表示
    if st.session_state['papers']:
        papers = st.session_state['papers']
        st.subheader('検索結果')
        for i, paper in enumerate(papers, start=1):
            title = paper['title']
            summary = paper['summary']
            published = paper['published']
            st.markdown(f'**論文 {i}**')
            st.write('**タイトル:**', title)
            st.write('**投稿日時:**', published)
            st.write('**要約:**', summary)
            st.write('---')
            
            
            if st.button(f'ChatGPTに質問する', key=f'question_button_{i}'):
                # 論文の要約を生成
                st.session_state['papers'][i-1]['paper_summary']  = generate_summary(summary)
                paper_summary = paper['paper_summary']
                st.write('**この研究を一言で表すと:**', paper_summary['short_summary'])
                st.write('**既存研究の問題点や課題は?:**', paper_summary['problem'])
                st.write('**この研究ではどのようなアプローチを行ったのか?:**', paper_summary['how'])
                st.write('**どのような結果や結論が得られたか?:**', paper_summary['result'])
        
​
            else:
                if 'paper_summary' in paper.keys():
                    # 生成された要約を表示
                    paper_summary = paper['paper_summary']
                    st.write('**この研究を一言で表すと:**\n', paper_summary['short_summary'])
                    st.write('**既存研究の問題点や課題は?:**', paper_summary['problem'])
                    st.write('**この研究ではどのようなアプローチを行ったのか?:**', paper_summary['how'])
                    st.write('**どのような結果や結論が得られたか?:**', paper_summary['result'])
        
​
                
​
​
​
if __name__ == '__main__':
    main()
​

作業ディレクトリにコマンドプロンプトで移動して、

streamlit run app.py

でローカルで実行できます。

やや回答が長いのと、一言で表した結果は微妙ですが、サクッと日本語で確認するにはいいかなという感じですね。 もう少しプロンプトを工夫すればよりよくなるかもしれません。

あとはGitHubに上げれば、Streamlitを使って簡単に公開もできます。 その場合は、requirement.txtを作成する必要があります。

streamlit
openai

今回のアプリはこちらに公開しています。 https://papersummary.streamlit.app/

今回のアプリではエラーや例外の処理など細かいところは実装していないので、使い方によってはエラーが出てしまうことがあるので注意してください。

今回のコード生成と記事作成では本当にChatGPTにお世話になりました。おかげでかなり短時間で作成できました。ただ、完璧なコードは出してくれないので、やはり現状ではプログラミング基礎知識は必要だと感じました。

参考サイト https://zenn.dev/turing_motors/articles/579ffa1c80661a https://qiita.com/PND/items/c9e9d449539a460515a4#function-calling

参考:簡単なコード解説

  1. ライブラリのインポートと変数の初期化:

import streamlit as st
import requests
import json
import datetime
import pytz
import xml.etree.ElementTree as ET
import openai

# arXiv APIのエンドポイント
arxiv_api_url = 'http://export.arxiv.org/api/query'

最初に必要なライブラリをインポートし、arXiv APIのエンドポイントのURLを指定しています。

XMLデータの解析:

def parse_xml(xml_data):
    # XMLデータを解析してタイトル、要約、投稿日時を抽出する
    root = ET.fromstring(xml_data)
    papers = []
    for entry in root.findall('{http://www.w3.org/2005/Atom}entry'):
        title = entry.find('{http://www.w3.org/2005/Atom}title').text
        summary = entry.find('{http://www.w3.org/2005/Atom}summary').text
        published = entry.find('{http://www.w3.org/2005/Atom}published').text
        papers.append({'title':title, 'summary':summary, 'published':published})
    return papers

parse_xml()関数は、arXiv APIから取得したXMLデータを解析し、各論文のタイトル、要約、投稿日時を抽出します。

arXiv APIで論文を検索する関数:

def search_arxiv_papers(query, start_date=None, end_date=None):
    # arXiv APIのエンドポイントとパラメータを指定
    base_url = 'http://export.arxiv.org/api/query'

    # クエリパラメータの設定
    params = {
        'search_query': f'{query} AND submittedDate:[{start_date} TO {end_date}235959]',
        'max_results': 5,  # 取得する論文の最大数
        'sortBy': 'relevance',  # 関連性に基づいてソート
        'sortOrder': 'descending',  # 降順にソート
    }

    # APIリクエストを送信してレスポンスを取得
    response = requests.get(base_url, params=params)

    if response.status_code == 200:
        # レスポンスのXMLデータを解析してタイトルと要約を取得
        xml_data = response.content
        papers = parse_xml(xml_data)
        return papers
    else:
        print('Failed to fetch papers from arXiv API.')
        return []

search_arxiv_papers()関数は、指定したクエリ、開始日、終了日に基づいてarXiv APIを使用して論文を検索します。パラメータを設定し、APIリクエストを送信し、レスポンスのXMLデータを解析して論文のタイトルと要約を取得します。

ChatGPT APIを使用して要約を生成する関数:

def generate_summary(text):
    # ChatGPT APIを使用して要約を生成
    functions = [
    {
        "name": "output_format",
        "description": "あなたは研究者です。以下の論文の要約文章を読んで、以下の4つの問いに日本語で答えてください。",
        "parameters": {
        "type": "object",
        "properties": {
            "short_summary": {
            "type": "string",
            "description": "この研究を一言で表すと",
            },
            "problem": {
            "type": "string",
            "description": "既存研究の問題点や課題は?",
            },
            "how": {
            "type": "string",
            "description": "この研究ではどのようなアプローチを行ったのか?",
            },
            "result": {
            "type": "string",
            "description": "どのような結果や結論が得られたか",
            },
        },
        "required": ["short_summary","problem", "how", "result"],
        },
    }
    ]
    # content = '''あなたは研究者です。以下の論文の要約文章を読んで、以下の問いに日本語で答えてください。
    # - この研究を一言で表現すると?
    # - 既存研究の問題点や課題は?
    # - この研究ではどのようなアプローチを行ったのか?
    # - どのような結果や結論が得られたか
    # '''
    if st.session_state["api_key"] == "":
        import pickle

        # 保存したresponseオブジェクトを読み込むファイルパス
        file_path = "../response.pkl"

        # pickleファイルからresponseオブジェクトを読み込む
        with open(file_path, "rb") as file:
            response = pickle.load(file)
    else:
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo-0613",
            functions=functions,
            messages=[
                # {"role":"system", "content":content},#精度はこれがある方がよい
                {"role": "user", "content": text},
            ],
        )
       
    
    output_msg = response["choices"][0]["message"]["function_call"]["arguments"]
    output_dict = json.loads(output_msg)
    return output_dict
    

generate_summary()関数は、与えられたテキストを使用してChatGPT APIを呼び出し、要約を生成します。API呼び出しには、要約に関する質問を含むメッセージと関数を設定します。

if st.session_state["api_key"] == "":
    import pickle

    # 保存したresponseオブジェクトを読み込むファイルパス
    file_path = "../response.pkl"

    # pickleファイルからresponseオブジェクトを読み込む
    with open(file_path, "rb") as file:
        response = pickle.load(file)

の部分は開発時に何度もOpenAIのAPIを使わないための処理です。 最後に基本的なページを構成しているメイン関数です。

def main():
    st.title('論文要約アプリ')

    #API入力
    api_key = st.text_input("OpenAI APIキーを入力してください", on_change=set_api, key='api_key')

最初の部分では、アプリのタイトルを表示し、OpenAI APIキーの入力フィールドを表示します。入力フィールドの値がない場合は、後ろでresponse.pklを使えるようにしています。(節約)

  # キーワードの入力
    query = st.text_input('キーワードを入力してください')
    # 検索期間の指定
    start_date_input = st.text_input('開始日をYYYYMMDD形式で入力してください(未入力の場合は直近1週間になります)')
    end_date_input = st.text_input('終了日をYYYYMMDD形式で入力してください(未入力の場合は現在日時になります)')
    if "papers" not in st.session_state:
        st.session_state['papers'] = None
    


次に、検索クエリと開始日・終了日の選択フィールドを表示します。

        # 検索ボタンが押された場合の処理
    if st.button('検索', key='search_button'):
        if query:
            
            # JST(日本時間)のタイムゾーンを設定
            jst = pytz.timezone('Asia/Tokyo')

            # 現在の日付を取得(JST)
            now = datetime.datetime.now(jst)

            # 1週間前の日付を計算
            one_week_ago = now - datetime.timedelta(weeks=1)

            # YYYYMMDD形式の文字列をdatetimeオブジェクトに変換
            start_date = start_date_input if start_date_input else one_week_ago.strftime('%Y%m%d')
            end_date = end_date_input if end_date_input else now.strftime('%Y%m%d')
            st.write(start_date)
            # arXiv APIを使用して論文を検索
            papers = search_arxiv_papers(query, start_date, end_date)
            if papers:
                st.session_state['papers']= papers
            else:
                st.warning('該当する論文が見つかりませんでした。')

論文の検索ボタンが押された場合、search_arxiv_papers()関数を使用してarXiv APIを呼び出し、指定されたクエリ、開始日、終了日に基づいて論文を検索します。

    # 論文の検索結果を表示
    if st.session_state['papers']:
        papers = st.session_state['papers']
        st.subheader('検索結果')
        for i, paper in enumerate(papers, start=1):
            title = paper['title']
            summary = paper['summary']
            published = paper['published']
            st.markdown(f'**論文 {i}**')
            st.write('**タイトル:**', title)
            st.write('**投稿日時:**', published)
            st.write('**要約:**', summary)
            st.write('---')
            
            
            if st.button(f'ChatGPTに質問する', key=f'question_button_{i}'):
                # 論文の要約を生成
                st.session_state['papers'][i-1]['paper_summary']  = generate_summary(summary)
                paper_summary = paper['paper_summary']
                st.write('**この研究を一言で表すと:**', paper_summary['short_summary'])
                st.write('**既存研究の問題点や課題は?:**', paper_summary['problem'])
                st.write('**この研究ではどのようなアプローチを行ったのか?:**', paper_summary['how'])
                st.write('**どのような結果や結論が得られたか?:**', paper_summary['result'])
        
​
            else:
                if 'paper_summary' in paper.keys():
                    # 生成された要約を表示
                    paper_summary = paper['paper_summary']
                    st.write('**この研究を一言で表すと:**\n', paper_summary['short_summary'])
                    st.write('**既存研究の問題点や課題は?:**', paper_summary['problem'])
                    st.write('**この研究ではどのようなアプローチを行ったのか?:**', paper_summary['how'])
                    st.write('**どのような結果や結論が得られたか?:**', paper_summary['result'])
        
​
                
​

それぞれの論文について、タイトル、投稿日時、abstが表示されます。generate_summary()関数を使用してChatGPTを呼び出し、要約を生成し、それも表示します。

いいなと思ったら応援しよう!