さて以前ネットで出会ったChatGPTによる論文要約のプログラムをいくつか試してみたことがありました。
最近プログラミングを始めたこともありまして、自分でも一度トライしてみようと思い立ち、ChatGPTと相談しながらいくつかpythonで試作してみました。
これは周知の事実だと思うのですが、pdfというファイル形式があまりにも扱いにくすぎることを初めて知りました。高度なpdfの解析は沼すぎて時間が無限にかかりそうだったので、本当にチープで簡素なプログラミングとなっております。
APIを使ってお金がかかるのが嫌な人向けにAPI使わずにコピペのゴリ押しでできるようなものも作ってみたので、そういったところも興味があれば是非ご利用ください。
前提としてpythonが使える方向け(自分の環境はjupyter notebook 6.5.2)ですが、一応pythonを実行する環境がなくてもGoogle Colaboratoryで使えるようにもしてみたので、ご参照ください。読み込むpdfファイルがローカルファイルだとできないので、アップロードという手間が必要な点は注意です。
なお、Google Colaboratoryの使い方および読み込みファイルのアップロードの仕方はこちらがわかりやすかったので参考にどうぞ。
【Colab 入門】Google Colaboratory とは?使い方・メリットを徹底解説! | キカガクの技術ブログ
ではコードについてみていきます。
※2023/6/3追記 読みたいpdfファイルをアップロードする以外にgoogle driveと連携をとれるとよりやり易かったので、google driveをマウントするコードも組み込みました。driveとのファイルの連携については外部のこちらのサイトもご参照ください。
【Google Colaboratory】Google ドライブにマウントし、ファイルへアクセスする方法
①ChatGPTのAPIを使用せずにチャットボックスにゴリ押しするコード
ここからのコードではpdfminerのライブラリを使っていますので、ない方は以下のコードでインストールをお願いします。
pip install pdfminer
早速まずは一つ目のコードから紹介します。調整可能な部分については後で記載します。
import io
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage
#pdfファイルをテキスト情報のみ抽出する関数
def extract_text_from_pdf(pdf_path):
resource_manager = PDFResourceManager()
fake_file_handle = io.StringIO()
converter = TextConverter(resource_manager, fake_file_handle)
page_interpreter = PDFPageInterpreter(resource_manager, converter)
with open(pdf_path, 'rb') as fh:
for page in PDFPage.get_pages(fh,
caching=True,
check_extractable=True):
page_interpreter.process_page(page)
text = fake_file_handle.getvalue()
converter.close()
fake_file_handle.close()
if text:
return text
#ファイル名の指定
pdf_path = 'pdfファイルのパス名を入力'
pdf_text = extract_text_from_pdf(pdf_path)
def split_text(pdf_text):
full_text = pdf_text.strip()
chunks = []
while len(full_text) > 0:
# 最初の5000文字を取得
first_part = full_text[:5000]
# 最後の"."で区切る
last_period_index = first_part.rfind(".")
if last_period_index != -1:
first_part = first_part[:last_period_index+1]
# 分割されたテキストに追加する
chunks.append(first_part)
# 残りの部分の最初の5000文字を取得
remaining_text = full_text[len(first_part):]
if len(remaining_text) > 0:
next_part = remaining_text[:5000]
# 最後の"."で区切る
last_period_index = next_part.rfind(".")
if last_period_index != -1:
next_part = next_part[:last_period_index+1]
chunks.append(next_part)
full_text = remaining_text[len(next_part):]
else:
break
return chunks
chunks = split_text(pdf_text)
# 分割したものを繰り返し出力する
for i in range(len(chunks)):
print(f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。\n{chunks[i]}\n要約したものは・を必ず先頭につけて箇条書きにしてください。\n")
【入力が必要な部分】
‘pdfファイルのパス名を入力’→要約したいpdfのパスを入力
first_part = full_text[:5000]、next_part = remaining_text[:5000]→区切りたい文字数を入力
単純にpdfのテキスト抽出し、指定の文字数単位内の最後のピリオドがある部分で切り取るプログラムです。以下のような文章が繰り返し出力されます。
以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。また翻訳しにくい専門用語は英語のままとしてください。 <要約したい論文を分割したテキスト> 要約したものは・を必ず先頭につけて箇条書きにしてください。
手動でpdfを直接コピペするよりは楽です。箇条書きを指示した命令文が最初と最後に入っていますが、片方だけだと無視されることがあるので現実と同様に何度もネチネチいうことが大事なようです。
実行すると細切れになったpdf内のテキスト情報が出力されます。あとはひたすらコピペしてChatGPTに要約させます。
なお、pdfの種類によっては単語間のスペースがなく出力されることがありますが、ChatGPT君は言語の天才なので問題ありません。何事もなく要約を進めてくれます。
例えば論文を入れると以下のような感じで要約されます。
・PSP(進行性核上性麻痺症)の初期症状は、転倒、歩行不安定、遅延運動、認知機能低下、意思決定機能の障害、言語障害、嚥下障害、眼球運動障害などがある。
・PSP-RS(Richardson症候群)の初期症状は、垂直性のサッケード眼球運動の減速、消失が見られる。
・PSP-P(パーキンソン症状型PSP)は、最初にパーキンソン症状を呈し、レボドパに対する反応があり、病気の進行も遅い。
指示からずれている部分もありますが、概ね安定しています。手間を省くには以下のコードの数値を変更することで、分割する文字数を増やすとコピペする回数が減って楽ですが、その分精度は落ちます。
②手動でキーワードから内容を分割し、チャットボックスにゴリ押しするコード
先ほどの①のコードは全テキスト情報をそのまま使いましたが、Background, Methods, Results, Discussionに応じてテキストを分割し、切り取ることもできます。そうすると、各セクションごとの内容をChatGPTに投げられるので、意味がまとまった内容を要約してもらうことができます。
ただ、ここでもpdfという形式の扱いにくさが問題で、段落ごとに綺麗に分けて認識する方法は困難なことも多く、また論文によってbackgroundではなくてintroductionだったり、順番が異なっていたり、バリエーションもあるので画一的なプログラムで対応しにくいようです。
ということでpdfの段落を認識させる方法はさっぱり諦めまして、テキストから文字列を検索して分割する、というしょっぱい方法を取ります。何がしょっぱいかと言えば、例えば”Methods”で区切る場合、他の部分に”methods”という言葉が出るとそこで誤って区切られてしまう点です。この場合、検索して○番目に出てきた場所などと指定するか、周辺の語句を拾って認識させるしかありません。
Abstractが冒頭にある場合、1番目に出てきた”methods”で分割すると誤った分割方法になるため、2番目に出てきたmethodsから区切り始める以下のようなコードにしてみました。
import io
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage
#pdfファイルからテキスト情報のみを抽出
def extract_text_from_pdf(pdf_path):
resource_manager = PDFResourceManager()
fake_file_handle = io.StringIO()
converter = TextConverter(resource_manager, fake_file_handle)
page_interpreter = PDFPageInterpreter(resource_manager, converter)
with open(pdf_path, 'rb') as fh:
for page in PDFPage.get_pages(fh,
caching=True,
check_extractable=True):
page_interpreter.process_page(page)
text = fake_file_handle.getvalue()
# close open handles
converter.close()
fake_file_handle.close()
if text:
return text
pdf_path = 'pdfファイルのパスを入力'
pdf_text = extract_text_from_pdf(pdf_path)
#文字列検索を使って分割、コピペしやすいように命令文と一緒に出力する
bstart_pos = pdf_text.find("Background")
bbend_pos = pdf_text.find("Methods")
bend_pos = pdf_text.find("Methods", bbend_pos + 1)
if bstart_pos != -1 and bend_pos != -1:
background_text = pdf_text[bstart_pos:bend_pos]
print(f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。Background:\n{background_text.strip()}\n要約したものは・を必ず先頭につけて箇条書きにしてください。")
bend_pos = pdf_text.find("Methods", bbend_pos + 1)
mmend_pos = pdf_text.find("Results")
mend_pos = pdf_text.find("Results", mmend_pos + 1)
if bend_pos != -1 and mend_pos != -1:
methods_text = pdf_text[bend_pos:mend_pos]
print(f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。\nMethods:\n{methods_text.strip()}\n要約したものは・を必ず先頭につけて箇条書きにしてください。")
mend_pos = pdf_text.find("Results", mmend_pos + 1)
rrend_pos = pdf_text.find("Discussion")
rend_pos = pdf_text.find("Discussion", rrend_pos + 1)
if mend_pos != -1 and rend_pos != -1:
results_text = pdf_text[mend_pos:rend_pos]
print(f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。\nResults:\n{results_text.strip()}\n要約したものは・を必ず先頭につけて箇条書きにしてください。")
rend_pos = pdf_text.find("Discussion", rrend_pos + 1)
if rend_pos != -1:
discussion_text = pdf_text[rend_pos:]
print(f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。\nDiscussion:\n{discussion_text.strip()}\n要約したものは・を必ず先頭につけて箇条書きにしてください。")
【入力が必要な部分】
‘pdfファイルのパス名を入力’→要約したいpdfのパスを入力
pdf_text.findの後にある””→検索して分割したい部位のワードを入力
手動で処理する手間が多いので、ちょっと微妙かもしれません。一応出力した結果は以下のようになります。
以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。 Background: <Backgroundの文章> 要約したものは・を必ず先頭につけて箇条書きにしてください。 以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。 Methods: <Methodsの文章> 要約したものは・を必ず先頭につけて箇条書きにしてください。 以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。 Results: <Resultsの文章> 要約したものは・を必ず先頭につけて箇条書きにしてください。 以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。 Discussion: <Discussionから最後まで> 要約したものは・を必ず先頭につけて箇条書きにしてください。
あとはこれをChatGPTに投げればよいのですが、文字数があまりに多い場合は指示がうまくいかなくなる時があるので注意が必要です。
③ChatGPTのAPI連携で分割したテキストを投げて結果をまとめるコード
一番楽ですが、API連携により有料となるコードです。実行すると概ね論文一つにつき0.2~0.4ドルほどのお金がかかりますので注意してください。なお、悲しいことにChatGPTに投げる段階まで到達しているとエラーが出てもお金は取られます。①で分割したテキストをひたすらChatGPTに投げ、返ってきた返答をつなげるという仕組みです。
なおAPIの利用にはopenaiのライブラリが必要なので、ない方は以下でインストールします。
pip install openai
ChatGPTのAPIは入力および出力のトークン(文字数などによる情報量)で料金が変わるので①のテキスト全抽出ではなく②などの方法を用いてもう少し文量を減らせると良いかもしれません。pdfファイルの解析が上達したらそのへんを改良したいところです。
コードはこちら。
import io
import openai
import re
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfpage import PDFPage
#pdfからテキスト情報のみ抽出する関数
def extract_text_from_pdf(pdf_path):
resource_manager = PDFResourceManager()
fake_file_handle = io.StringIO()
converter = TextConverter(resource_manager, fake_file_handle)
page_interpreter = PDFPageInterpreter(resource_manager, converter)
with open(pdf_path, 'rb') as fh:
for page in PDFPage.get_pages(fh,
caching=True,
check_extractable=True):
page_interpreter.process_page(page)
text = fake_file_handle.getvalue()
# close open handles
converter.close()
fake_file_handle.close()
if text:
return text
pdf_path = 'pdfファイルのパスを入力'
pdf_text = extract_text_from_pdf(pdf_path)
def split_text(pdf_text):
full_text = pdf_text.strip()
chunks = []
while len(full_text) > 0:
# 最初の1500文字を取得
first_part = full_text[:1500]
# 最後の"."で区切る
last_period_index = first_part.rfind(".")
if last_period_index != -1:
first_part = first_part[:last_period_index+1]
# 分割されたテキストに追加する
chunks.append(first_part)
# 残りの部分の最初の1500文字を取得
remaining_text = full_text[len(first_part):]
if len(remaining_text) > 0:
next_part = remaining_text[:1500]
# 最後の"."で区切る
last_period_index = next_part.rfind(".")
if last_period_index != -1:
next_part = next_part[:last_period_index+1]
chunks.append(next_part)
full_text = remaining_text[len(next_part):]
else:
break
return chunks
chunks = split_text(pdf_text)
# ChatGPTのAPIキー
openai.api_key = "API_keyを入れてください"
def summarize_text(pdf_text):
# 分割されたテキストデータを要約して出力する
summary = ""
for i in range(len(chunks)):
response = openai.Completion.create(
engine="text-davinci-002",
prompt=f"以下の文章は医学論文の一部です。あなたは医師でこれを日本語で要約してもらいます。要約したものは・を必ず先頭につけて箇条書きにしてください。\n{chunks[i]}\n要約したものは・を必ず先頭につけて箇条書きにしてください。",
max_tokens=100,
n=1,
stop=None,
temperature=0.5,
)
# 要約結果をsummaryに追加する
summary += re.sub('[\n.]', '', response.choices[0].text+ '\n')
summary += "\n"
return summary
final_summary=summarize_text(pdf_text)
print(final_summary)
【入力が必要な部分】
‘pdfファイルのパスを入力’→要約したいpdfのパスを入力
first_part = full_text[:5000]、next_part = remaining_text[:5000]→区切りたい文字数を入力
“API_keyを入れてください”→ChatGPTのAPIキーを入れます
※API keyが分からない方は前回記事も参照ください
プログラミング素人がChatGPTによる医学論文要約&メール通知を使ってみた
ChatGPTは一回の入力で2048トークンまでしかできないようで、エラーとならないように安全を取って1500文字での分割をしています。文章が途切れないように1500文字のなかで一番後ろにあるピリオドで切れるように指定しています。意味のまとまった部分で分割しているわけではないので、どうしても要約された文章の内容は不安定になりがちです。また時折プロンプト文の混入や英語のままとなった文章が見られることがあります。
出力は理想的には以下のようになります。
・<要約された文章>・<要約された文章>...
続いて実際に使用してみた例をみていきますが、なかなかこれも綺麗にはいかないです。
実際の使用例とかかった値段
さて、実際それぞれ使ってみるとどんな感じになるのでしょうか。手元にある論文を例にとってみてみます。
脳神経内科の人以外あまり興味がないかもしれない(!)進行性核上性麻痺のreviewです。観察研究やランダム化比較試験は見るべき情報が定まっているのでそこまで疲れませんが、レビューは流れが掴みにくいので読むのが苦手です。レビューですと②の形式は使えないので①と③を使ってみましょう。
方法①の使用例
①は手作業が必要ですが出力はかなり安定している印象です。そして無料。5000文字ごとで出力してコピペした際のChatGPTからのレスポンスは以下のようになりました。
・進行性核上性麻痺(PSP)は、運動異常以外にも行動、言語異常が含まれる症候群であることが分かった。
・PSPはタウタンパク質の異常と関連があり、新しいタウタンパク質治療の臨床試験に興味が集まっている。
・PSP-RSは稀な病気で、全体の患者数に占める割合は限られている。
・MDS PSP診断基準は、PSPの診断の早期段階を認識するために重要な進歩である。
読みやすく、良い感じにまとまっています。返答もすべて分割されてしまうので最後に改めてまとめてもらうとなお良いのかもしれません。
方法③の使用例
③は分割する文字数とプロンプトによって出力が変わってきますので、扱いが難しいです。プログラム中のtemperatureによっても内容は変わるのではないかと思います(高ければランダム性が高く、低ければランダム性の低い内容となる)。また出力のトークンをmax tokenというパラメータで指定できますが、これも低すぎると文章が途中で途切れてしまっていたりするので注意が必要です。
いくつかパラメータを調整してやってみた結果を以下に書いてみます。
-5000文字分割、max token 100
・PSPは最初は特異的なパーキンソン病の原因と考えられていましたが、過去10年間でPSPは行動、言語、運動の異常を含む範囲の内のクリニカルフェノ
・PSPは、PSP-RSの初期症状のフェーズであると考えられており、PSP症候群として完全に症状が現れる前に起こる。・PSPは、PSP-RSやvPSP症
・PSPの特徴的な症状は、直立した姿勢で視線を上方に動かす能力の消失である。・PSPは、パーキンソン病と似た症状を呈するため、早期に診断
5000文字だと上限に引っかかる可能性が高く、ちょっと無理をしていますがなぜだか通りました。分割が大きい分要約全体の量は少なくなっていますが、途中で切れている文章も散見されます。max tokenを広げないとこうなってしまうのかもしれません。料金は0.21ドルでした。
-2500文字分割、max token 50
・PSP-RSは1996年に発表された症状をもつPSPの症例である。・最もよく報告されている症状は、不明な転倒、不安定な歩行、ブラadykinesia、微妙
翻訳が難しい専門用語は英語のままでも良いです。・PSP-PGFは進行性歩行障害を特徴とし、開始遅延やその後の歩行停止、時には
PSP-CBS、PSP-SLはPSPの可能性、または確実な4リピートタウ病として指定されています。 これは、臨床的な症状に基づいてPSP病理学を決定する
2500文字だと要約された出力も増えてきます。精度が上がっているかと言われると微妙なところで意味不明な翻訳も増えています。上のものと異なり、プロンプト文に「翻訳が難しい専門用語は英語のままとします。」という文を入れてみたのですが、なぜかその文章が混入したり、精度が落ちていますね。料金は0.27ドルでした。
-1500文字分割、max token 200
・PSPは最初は非典型パーキンソン病と考えられていましたが、過去10年間では、行動、言語、および運動の異常を伴う幅広いクリニカルフェノタイプを含むことがわかってきました。・PSP-RSは1964年にスティール、リチャードソン、およびオルズェフスキーによって最初に記載されました。・PSPは、病理学的に、遺伝的にも
さらに分割する文字数を減らし、max tokenを広げて、かつ要約する文字数を100文字以内にしてみましたが、あまり聞き入れてもらえてないようです(上記の文章は156文字)。
まとめ
無料でやる方法①は手間がかかる分、結果も安定していますし、これでもいいんじゃないかと思ってしまいました。方法③はファイルを放り投げるだけなので楽ですが、まだまだ結果が読みにくかったりうまくいかない場合も多いですし、この点に関してはpdfの解析を十分にできている場合でも克服がまだ難しそうです。
しかしそのうち(というかすでにある気がしますが)有償でpdfの要約をしてくれるものとかあるんじゃないでしょうか。
テキストファイルで遊べるようになっただけでもとりあえず満足しましたがpdfの扱いはまだまだ奥が深そうですね。テキストの抽出を行う良い練習にはなったのでまた精進します。
コメントを残す