tpc29-1

TOPIC 29|Pythonで活用する:応用編|PlateauKit + PlateauLabを用いたAI連携とアプリ構築[1/2]|LLMと連携して操作する

PlateauKitを使うことで、Python上でPLATEAUの3D都市モデルを他のさまざまなライブラリと連携して処理できます。このトピックでは、PLATEAUの3D都市モデルとPythonの豊富なライブラリを組み合わせることで、どのようなことができるかを見ていきます。

Share

PlateauKitを使うことで、Python上でPLATEAUの3D都市モデルを他のさまざまなライブラリと連携して処理できます。このトピックでは、PLATEAUの3D都市モデルとPythonの豊富なライブラリを組み合わせることで、どのようなことができるかを見ていきます。

このトピックの内容は「PlateauKit+PlateauLabを活用してPythonで3D都市モデルをインタラクティブに扱う」(2024年度PLATEAU Hands-onアーカイブ動画)でも制作方法をハンズオン形式で紹介しています。

【目次】

29.1   このトピックの見どころ

29.2   LLMと連携して日本語で操作する

 29.2.1 LLMについて

 29.2.2 セットアップ

 29.2.3 LLMでコードを生成・実行する

 29.2.4 詳しいプロンプトの作成と実行 

 29.2.5 コードの実行結果を可視化する

29.1 _ このトピックの見どころ

PlateauKitを使うことで、Pythonから簡易にPLATEAUの3D都市モデルを扱えるようになります。その実例として、このトピックでは、以下の2つの応用例を解説します。 

① AIを用いたチャットで目的の地物を抽出する

Pythonは機械学習やAIプログラミングにも広く用いられており、LLMを用いたプログラミングも比較的容易に行うことができます。その実例として、「○○を抽出してください」などと日本語で指示を出すことで3D都市モデルから目的の地物を抽出するコードを解説します。このチュートリアルでは、LangChainというライブラリを用います。 

図29-1 LLMを用いて、日本語で地物の抽出ができるようにした例 

UIウィジェットと組み合わせて、ユーザーが操作可能な簡易アプリを構築する

JupyterLabやJupyter Notebook といったJupyter環境用のライブラリのひとつであるipywidgets(Jupyter Widgets)を使うと、Jupyter環境上でスライダーやボタンなどを備えたインタラクティブなUIを作れます。その実例として、任意の浸水継続時間を指定できるスライダーをUIとして作り、時間の経過に伴ってどの範囲に影響があるのかを可視化して確認できるインタラクティブなアプリを構築します。

図29-2 画面上部に浸水継続時間を調整できるスライダーを作り、インタラクティブに操作できるようにしたアプリの例

29.2 _ LLMと連携して日本語で操作する

ここでは、ChatGPTやMicrosoft Copilotに代表される、いわゆるLLM(大規模言語モデル)のAPIを使った簡易なアプリケーションを作成します。

LLMと連携すれば、日本語による指示で3D都市モデルから目的の地物を抽出するなどの操作が可能となります。

29.2.1 _ LLMについて

LLMは、「プロンプト」と呼ばれる入力データ(質問や指示などのテキストや画像)に対して応答を生成するAIモデルです。LLMには、各社が提供するWeb APIを利用するものと、ローカル環境で直接LLMを実行するもの(ローカルLLM)とがあります。 

PlateauKitには、LLMとの連携機能が実験的に実装されていますが、その基本的な仕組みを理解するために、このチュートリアルでは同様の機能を簡易的に再現して自分で実装してみます。

【メモ】

執筆時点(バージョン0.17.3)でのPlateauKitのLLM連携の実装は以下のリンクから参照できます。

【plateaukit/core/area/_llm.py】
https://github.com/ozekik/plateaukit/blob/v0.17.3/plateaukit/core/area/_llm.py

このチュートリアルでは、ChatGPTの開発元であるOpenAI社が提供するLLMのひとつ、GPT-4o-miniを同社のWeb API(OpenAI API)を通じて利用する例について説明します。OpenAI APIを利用するにはアカウント登録とログインが必要です。APIキーはログイン後に以下のページから発行できます。

【メモ】

OpenAI APIの利用は有償の従量課金です。使用量や料金に注意してください。

【OpenAI APIのAPIキー管理ページ(要ログイン)】
https://platform.openai.com/api-keys

29.2.2 _ セットアップ

このチュートリアルでは、LangChainというオープンソースのPythonライブラリを利用します。LangChainは、LLMを用いたアプリケーションの作成を簡易化する目的で設計されたフレームワークです。

TOPIC28「Pythonで活用する:基本編|PlateauKit+PlateauLabで始めるPLATEAU」で説明したように、PlateauKitでは3D都市モデルの建築物などをPandasのデータフレームとして扱います。こうしたPandasのデータフレームをLLMで扱うためには、LLMに与える指示(プロンプト)やデータの形式、返答の処理などを工夫する必要がありますが、LangChainを使うことで、一連の処理を比較的容易に実装できます。

【LangChain】
https://python.langchain.com/

次の手順で、LangChainを用いてOpenAI社のGPT-4o-miniを利用する準備をします。APIキーが必要なので、あらかじめ発行しておいてください。

【手順】LangChainをインストールし、GPT-4o-miniを使えるようにする

[1]LangChainのパッケージをインストールする

まず、Google Colabの環境上にLangChainおよび関連するパッケージをインストールしましょう。セルを追加して、次のマジックコマンド(「%」で始まるコマンド)を実行します。

%pip install -q langchain==0.2.12 langchain_experimental==0.0.64
langchain_openai==0.1.22

[2]APIキーの設定

次に、利用するAPIに関する設定をします。OpenAI APIを利用する場合は、取得したAPIキーをOPENAI_API_KEYという環境変数に設定します。セルを追加して、次のコマンドを実行します。

%set_env OPENAI_API_KEY=ここにAPIキー

【メモ】

APIキーは機密情報です。誤って公開したりしないよう注意してください。このセルは実行後すぐに削除することをおすすめします(一度実行すれば、セルを削除しても実行環境がリセットされるまで設定は有効です)。

【メモ】

予定しているAPIの利用が終わったら、OpenAIの管理ページでAPIキー自体を削除しておくとより安全です。

コラム:シークレット機能を使う

Google ColabにはAPIキーのような秘密情報を保存する「シークレット機能」があり、左側メニューの鍵アイコンをクリックして表示される画面から秘密情報を保存できます。保存したデータは、以下のようなコードで取得して環境変数に設定できます。より安全に使うには、この方法も検討してください。

import os
from google.colab import userdata
os.environ["OPENAI_API_KEY"] = userdata.get("<保存したときの名称>")

リスト29-1 保存したシークレット情報を使う例

図29-3 シークレット機能

[3]動作確認する

APIキーを設定できたら、LangChainのLLMオブジェクトが作成できることを確認します。セルを追加して、次のコードを実行します。図29-4のようにChatOpenAIオブジェクトが表示されればAPIキーが設定できています。

from langchain_openai import ChatOpenAI 
llm = ChatOpenAI(model="gpt-4o-mini")
llm

リスト29-2 LLMオブジェクトを作成する

図29-4 実行結果

29.2.3 _ LLMでコードを生成・実行する

LangChainは、LLMと「ツール」(tool)を組み合わせることで、LLMの応答を元にさまざまな処理を行うという考え方で設計されています。さらに、「パーサー」(parser)を利用することで、使用したいツールに合わせて、LLMの応答を適切な形に整形できます。これらを組み合わせたものを「チェーン」(chain)と呼びます。

図29-5 LangChainにおけるチェーン

LangChainにおけるツールは、結果を元に実行するプログラムのコードです。自ら記述することもできますが、LangChainには「検索エンジンで検索する」「Wikipediaで検索する」「コマンドを実行する」などのツールがあらかじめ用意されています。

今回は、LLMに対して例えば「ヒカリエを抽出してください」(渋谷ヒカリエは渋谷に実在する商業施設)というプロンプトを与えると、LLMが3D都市モデルの建築物データが入ったPandasデータフレームから渋谷ヒカリエのデータを抽出するようなPythonのコードを出力し、それを実行することで結果が得られるという一連の流れを実現できるようにします。

つまり、LLMにプロンプトをもとにPythonのコードを生成させ、それを実行する構成になりますが、こうした目的のツールとしてLangChainにはPythonAstREPLToolというツールが用意されているので、今回はこれを利用します。

LLMが出力したPythonコードを実行するというところまで一気に進めるとわかりにくいので、以下では、「LLMにPythonのコードを生成させる」「生成されたコードを実行する」という2つのステップに分けて説明します。

① Pythonのコードを生成させる

まずはLLMでPythonのコードを生成するところまで進めましょう。セルを追加して次のコードを実行すると、LLMが提案したコードが表示されます。

from langchain_core.output_parsers.openai_tools import JsonOutputKeyToolsParser
from langchain_experimental.tools import PythonAstREPLTool

# 範囲を選択、データフレームをコピー
area = dataset.area_from_landmark("渋谷駅")
df = area.gdf.copy()

# PythonAstREPLTool を利用する
tool = PythonAstREPLTool(locals={"df": df}, verbose=True)
llm_with_tools = llm.bind_tools([tool], tool_choice=tool.name)
parser = JsonOutputKeyToolsParser(key_name=tool.name, first_tool_only=True)

# LLMに対する入力内容
query = """あなたは `df` という pandas のデータフレームにアクセスできます。\
このデータフレームの `name` の一覧を出力してください。
"""

# チェーンの作成
chain = llm_with_tools | parser

# APIの呼び出し
response = chain.invoke(query)
response

リスト29-3 プロンプトを渡して、その目的を実現するためのPythonコードを出力させるコード

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

図29-6 LLMが生成したPythonコードが表示された(「df['name'].unique()」がコード部分)

このコードでは、まず、渋谷駅周辺の3D都市モデルの建築物データが格納されたデータフレームをコピーして変数「df」に代入しています。

# 範囲を選択、データフレームをコピー
area = dataset.area_from_landmark("渋谷駅")
df = area.gdf.copy()

リスト29-3より抜粋

そして、この変数dfを引数に指定して、生成されたPythonコードを実行するためのツール(PythonAstREPLToolオブジェクト)を作成しています。デフォルトでは、生成されたコードを実行する際に、そのコード外の変数にはアクセスできませんが、引数localsにアクセスを許可する変数を辞書として指定することで、指定した変数にツールからアクセスできるようになります。

# PythonAstREPLToolを利用する
tool = PythonAstREPLTool(locals={"df": df}, verbose=True)
llm_with_tools = llm.bind_tools([tool], tool_choice=tool.name)

リスト29-3より抜粋

次のJsonOutputKeyToolsParserは、OpenAI APIを使用する場合の専用のパーサーで、引数key_nameに指定した特定のツール(今回は先ほど作成した「PythonAstREPLTool」)に渡せる形式に実行結果を変換します。今回は1つのツールしか使用しないので、引数first_tool_onlyにTrueを指定しています。

parser = JsonOutputKeyToolsParser(key_name=tool.name, first_tool_only=True)

リスト29-3より抜粋

これでLLM、ツール、パーサーの準備ができました。以下の抜粋は、これらを組み合わせてチェーンを作成し、実行している部分です。

# LLMに対する入力内容
query = """あなたは `df` というpandasのデータフレームにアクセスできます。\
このデータフレームの `name` の一覧を出力してください。
"""

# チェーンの作成
chain = llm_with_tools | parser

# APIの呼び出し
response = chain.invoke(query)
response

リスト29-3より抜粋

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

ここでqueryは、LLMに渡すプロンプト(入力)です。つまり、「あなたはdfというpandasのデータフレームにアクセスできます。このデータフレームのnameの一覧を出力してください。」と渡しています(ただしこの段階では、データフレームの内容自体をLLMに渡しているわけではないことに注意してください)。

これがLLM(gpt-4o-mini)に渡されて、これを実現するようなコードとして、実行結果にある次のコードが返されます。

df['name'].unique()

ここでは試しませんが、queryに指定する日本語による指示を変更すれば、さまざまなPythonのコードを出力できます。

② コードを実行する

次に、LLMが生成したコードを実際に実行します。①の操作において、コードが実行されずに表示されているのは、チェーンの最後にツールを指定していないからです。次のように、ツール(tool変数)も追加したチェーンを作って、そのinvokeメソッドを呼び出すようにすると、コードが実行されるようになり、コードの実行結果が表示されます。

【メモ】

PythonAstREPLToolはLLMが生成したコードを実行しますが、このときLLMが好ましくないコード(例えばセキュリティ上、問題があるコードなど)を出力する可能性もあります。安全のためには、信頼できるコードであることを確かめてから実行するか、実行しても問題ない環境であることを確認してください。

# チェーンの作成
chain = llm_with_tools | parser | tool

# APIの呼び出し
response = chain.invoke(query)
response

リスト29-4 ツールを追加したチェーンを作り、生成されたコードを実行する

図29-7 LLMが出力したPythonコードが実行され、その結果が表示された

29.2.4 _ 詳しいプロンプトの作成と実行

今の例では、次のように、LLMにはプロンプトとして最低限の指示しか与えていません。

query = """あなたは `df` というpandasのデータフレームにアクセスできます。\
このデータフレームの `name` の一覧を出力してください。
"""

リスト29-3より抜粋

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

指示に加えて、データフレームに含まれる項目の情報といった前提知識や、満たすべき条件のような設定を与えることで、より高度な処理が可能になります。

以下は、詳しい前提知識や設定を含めた詳細なプロンプトを作成する例です。このプロンプトでは、先ほどと異なり、データフレーム(変数df)の列名や内容の一部などもLLMに渡しています。ただし、列「geometry」にあるジオメトリ情報(形状)はサイズが大きくなるため、この列は除外しています。

このプロンプトを利用して新しい指示を出してみましょう。セルを追加して、次のコードを実行します。

from langchain_core.prompts import ChatPromptTemplate

# 文字列に波括弧が含まれる場合にエスケープする関数
def escape_braces(text):
  return text.replace("{", "{{").replace("}", "}}")

# 詳細なプロンプト
system = f"""あなたは `df` というpandasのデータフレームにアクセスできます。\
以下は `df.drop(columns="geometry").head().to_markdown()` の実行結果です:
{escape_braces(df.head().to_markdown())}
以下はこのデータフレームの項目の一部の説明です:
- buildingId: 建築物のID
- name: 建築物の名称。常にこの項目から建築物の名称を探してください
- measuredHeight: 建築物の計測高さ
- storeysAboveGround: 地上階数
- storeysBelowGround: 地下階数
- usage: 建築物の用途
- longitude: 建築物の緯度
- latitude: 建築物の経度
以下はこのデータフレーム内に名称が存在する建築物の一覧です:
{df['name'].unique()}
以下はこのデータフレーム内に存在する建築物の用途の種別一覧です:
{df['usage'].unique()}
ユーザーの指示に対して、Pythonのコードを出力してください。\
それ以外は何も出力しないでください。\
座標に関して知識を用いず、データフレームの情報を用いてください。\
Pythonの標準ライブラリとpandas, geopandasだけがライブラリとして利用できます。
必ずpandasのデータフレームを返すPythonのコードを書いてください。\
"""

# LangChain用に変換
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{user_input}")])

# チェーンの作成
chain = prompt | llm_with_tools | parser | tool

# APIの呼び出し
response = chain.invoke("ヒカリエを抽出してください。")
response

リスト29-5 PLATEAUデータセットが含まれたデータフレームを渡し、プロンプトから操作する例

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

図29-8 実行結果

この例では、次のように「ヒカリエを抽出してください」というプロンプトを与えていますが、プロンプトを変えることで別の処理を実行することができます。このように、LLMを利用することで、与えるプロンプト次第で自分でコードを書き換えなくてもさまざまな処理を実行できます。

# APIの呼び出し 
response = chain.invoke("ヒカリエを抽出してください。")

リスト29-5より抜粋

また、LLMのメリットとして、あいまいな表現などからでも一定の柔軟な処理が可能な点も挙げられます。例えば、実はこのPLATEAUデータセットに含まれている正式名称は「渋谷ヒカリエ」なのですが、LLMが「ヒカリエ」を「渋谷ヒカリエ」として解釈して処理を実行しています。

ただし、LLMは常に指示どおりの応答を生成してくれるとは限らないことに注意してください。LLMが適切な応答を生成できずエラーとなることや、誤った結果を返すこともあります。そうした場合、プロンプトや問い合わせ内容を工夫することで、よりよい結果が得られる可能性があります。

また、PLATEAUに含まれている属性データ以外のデータとも組み合わせることで、さまざまな情報を使った処理を行うことができます。例えば、 TOPIC28のコラム「他のデータと組み合わせる」のように自治体のオープンデータと組み合わせて、フリーWi-Fiの提供有無などの詳しい条件から施設を探したり、OpenStreetMapのOverpass API図書館APIなどの外部のAPIと連携して、指定した場所や範囲についての情報を取得したりできます。

29.2.5 _ コードの実行結果を可視化する

前節ではLLMの処理結果をデータフレームとして取得しましたが、これをさらにマップやグラフとして表示してみましょう。

まず、抽出した建築物のみをマップ上に表示してみます。セルを追加して、次のコードを実行します。

from plateaukit.core.area import Area, GeoDataFrameLayer

response = chain.invoke("ヒカリエを抽出してください。")

new_df = area.gdf[area.gdf["buildingId"].isin(response["buildingId"])]
response_area = Area(GeoDataFrameLayer(new_df), base_layer_name="bldg")
response_area.show()

リスト29-6 抽出した建築物をマップに表示する例

図29-9 抽出した建物をマップに表示した

今度は、別のプロンプトを送ってみましょう。セルを追加して、次のコードを実行します。

response = chain.invoke("高さトップ50の建物を抽出してください") 
response

リスト29-7 高さトップ50の建物を抽出する例

図29-10 高さトップ50の建物を抽出実行結果

この結果をグラフ化してみます。セルを追加して、次のコードを実行します。

import plotly.express as px 

fig = px.bar(response, x="buildingId", y="measuredHeight", text="name")
fig.show()

リスト29-8 リスト29-7の結果をグラフ化する

図29-11 グラフ化した様子

プロンプトの工夫によって、LLMに直接グラフを作成させるといった高度な利用も可能です。さまざまな使い方を試してみてください。

【文】

小関 健太郎

【協力】

大澤文孝