エンジニアリングとお金の話

都内で働くエンジニアの日記です。

共通的に使用するpythonコマンドはpipxで管理することにした

pythonで環境を構築する際に常に悩んでいたのが、共通的に使用したいpythonコマンドのインストール方法である。具体的にはPoetryやJuypter labなど。 今まではpyenvでインストールしたpython環境にpipで導入していたが、なんか気持ち悪い。良い解決方法が無いか探していたらpipxなるものを見つけた。

github.com

pipxを使用すると共通的に使用したいpythonコマンド毎に仮想環境を構築してインストールを行う。これでpyenvでインストールした環境をクリーンに保つことができる。

install方法は以下の通り

brew install pipx
pipx ensurepath

使用方法は以下の通り

# インストール
pipx install poetry

# インストールリスト表示
pipx list

# インストール資産アップデート
pipx upgrade poetry

#インストール資産削除
pipx uninstall poetry

インストールした資産は以下の通り

package jupyter 1.0.0, installed using Python 3.12.1
 - ipython
 - ipython3
 - jlpm
 - jsonpointer
 - jsonschema
 - jupyter
 - jupyter-console
 - jupyter-dejavu
 - jupyter-events
 - jupyter-execute
 - jupyter-kernel
 - jupyter-kernelspec
 - jupyter-lab
 - jupyter-labextension
 - jupyter-labhub
 - jupyter-migrate
 - jupyter-nbconvert
 - jupyter-notebook
 - jupyter-qtconsole
 - jupyter-run
 - jupyter-server
 - jupyter-troubleshoot
 - jupyter-trust
 - normalizer
 - pybabel
 - pygmentize
 - pyjson5
 - qtpy
 - send2trash
 - wsdump
 - man1/ipython.1
package poetry 1.7.1, installed using Python 3.12.1
 - poetry
package powerline-shell 0.7.0, installed using Python 3.12.1
 - powerline-shell
package python-dotenv 1.0.1, installed using Python 3.12.1
 - dotenv

注意点としてpipxでjupyterをインストールする時は--include-depsを付ける必要がある。

pipx install --include-deps jupyter

※参考URL

Pythonの便利ツールpipx

MacにPythonの環境構築をするお話 2020年版 | gadgetlunatic

response_formatとfunction_callingのjson出力の違いについて

先日OpenAI DevDayで発表があったresponse_formatを早速使用してみました。

response_formatはプロンプトにjson形式を指定することで出力がJSON形式に固定されるモードになります。使い方は簡単で以下条件を満たせばOKです。

  • モデルをgpt-4-1106-previewかgpt-3.5-turbo-1106にする
  • systemプロンプトにjson形式で出力することを記載する
  • パタメータにresponse_format={ "type": "json_object" }を追加する

今まではjson形式で出力を行う時はfunction_callingを使用していたのですが、それに比べると非常に簡単にjson形式で出力を行うことが可能となりました。これ幸いと今までfunction_callingで記載していた処理を書き換えたのですが以下の問題が発生しました。

出力する型が固定されない

function_callingでjsonを出力して時は下記の通り型をboolean型で指定していたので出力結果もboolean型で返却されていたのですが、response_formatで指定した時はboolean型だったり、文字型だったりと混在している感じでした。

function_callingの指定

functions =[
    {
        "name":"dummy_func",
        "description":"true or falseを受け取る",
        "parameters":{
            "type":"object",
            "properties":{
                "flg":{
                    "type":"boolean"
                }
            }
        },
        "required": ["flg"],
    }
]

function_callingの出力

{ "flg": true }

response_formatの指定(プロンプト)

出力形式はJSONでkey名は"flg"、値は"true"又は"false"として下さい。

response_formatの出力

{ "flg": false }
{ "flg": "false" }

モデルをgpt-3.5-turbo-1106にするとfunction_callingが空を返すようになった

response_formatだと出力が安定しなかったのでモデルをgpt-3.5-turbo-1106でfunction_callingでjsonを返却するように処理を実施すると、稀に空が返却されるようになりました。

function_callingの出力

{"flg":true}
{}

まとめ

response_formatは便利だけど、出力結果の型が重要なもの(boolean型や数値型など)についてはfunction_callingを使用した方が良いのではと思いました。プロンプトの指定でここらへんが制御出来るか試してみようと思います。

LangChainが発行しているプロンプトを確認する方法について

LangChainは非常に便利だけど自分が意図しないプロンプトが作成されていることがある。確認する方法が無いか探していたらログに出力出来ることが分かった。下記コードを追加することでログ上にLangChainが発行するプロンプトが出力される。

import openai
openai.log="debug"

上記のコードはLangChainがOpenAIが提供しているopenai-python SDKを使用いるため、openai-pythonのログ出力設定を変更することでプロンプト内容が表示されるという感じである。下記のyoutubeを参考した。

www.youtube.com

langchainのLLMとチェーンの使い分けについて

langchainには様々なモジュールが存在するが各々の関係性が良く分かって無かったので調査した。

langchainのモジュール

主なモジュールは以下の通り

  • LLM : 言語モデルによる推論の実行。
  • プロンプトテンプレート : ユーザー入力からのプロンプトの生成。
  • チェーン : 複数のLLMやプロンプトの入出力を繋げる。
  • エージェント : ユーザー入力を元にしたチェーンの動的呼び出し。
  • メモリ : チェーンやエージェントで状態を保持。

良く分からなかったこと

LLMでもチェーンでも同じような処理が出来るので?となっていた。

LLMで実行

from dotenv import load_dotenv
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

load_dotenv()

llm = OpenAI()
template = PromptTemplate(input_variables=["taste"], template="{taste}朝ごはん")
print(llm(prompt=template.format(taste="日本の")))

実行結果

日本の朝ごはんには、白米やご飯のおかずとして、味噌汁や煮物、おかか、魚介類、野菜などがあります。お弁当やおでんなどもあります。また、朝ごはんには、お茶や味噌汁、卵焼きなどの日本風のおかずを組み合わせて食べることもあります。特に、日本の小さな家庭では、白米を主食として、ご飯を炊いて、味噌汁や煮物などをご飯と一緒に食べる

チェーンで実行

from dotenv import load_dotenv
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain

load_dotenv()

template = PromptTemplate(input_variables=["taste"], template="{taste}朝ごはん")
chain = LLMChain(llm=OpenAI(), prompt=template)
print(chain.run("日本の"))

実行結果

日本の朝ごはんは、ご飯、味噌汁、煮物、おにぎり、サラダ、漬物などがあります。また、卵料理や魚料理などもあります。

複数のLLMとプロンプトの入出力を繋げたい場合はチェーンを使う

チェーンを使用することでLLMとプロンプトを1つのチェーンとして論理的にまとめることが出来る。チェーンは名前の通り別のチェーンとつなげる事ができるので、例えばチェーン1で出力した結果をチェーン2で使用したい場合などは、チェーンにて処理を構築すると良い。

langchainでチェーン同士をつなげる

langchainでチェーン同士をつなげると最初のチェーンが導き出したワードを元に、後続のチェーンがワードを導き出すことが出来る。チェーンをつなげる方法は2種類あり、SimpleSequentialChainは入出力を1つずつ持つチェーンを繋げるチェーン、SequentialChainは入出力を複数持つチェーンを繋げることができる。

SimpleSequentialChainサンプル

from dotenv import load_dotenv
from langchain.chains import LLMChain, SimpleSequentialChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

load_dotenv()

llm = OpenAI(temperature=0.9)

template = """\
あなたは料理家です。食材を与えるので料理を考えてください

食材:{food}
料理:\
"""

prompt_template = PromptTemplate(input_variables=["food"], template=template)

first_chain = LLMChain(llm=llm, prompt=prompt_template)


template = """\
あなたは栄養士です。料理を与えるので栄養価を教えて下さい

料理:{dish}
レビュー:\
"""

prompt_template = PromptTemplate(input_variables=["dish"], template=template)

second_chain = LLMChain(llm=llm, prompt=prompt_template)

overall_chain = SimpleSequentialChain(chains=[first_chain, second_chain], verbose=True)

review = overall_chain.run("卵")

print(review)

実行結果

Entering new SimpleSequentialChain chain... オムレツ

オムレツは、カロリーが高くないため、たんぱく質や鉄分など栄養価が高い食品としても有名です。1食あたりのカロリーは約400cal、たんぱく質は約20g、鉄分は約1.5mgです。また、ビタミンB12なども含まれています。卵を中心としたこの料理は、糖質も低く、低血糖を抑えながら長時間エネルギーを支えてくれるため、健康に良い料理といえます。

Finished chain.

オムレツは、カロリーが高くないため、たんぱく質や鉄分など栄養価が高い食品としても有名です。1食あたりのカロリーは約400cal、たんぱく質は約20g、鉄分は約1.5mgです。また、ビタミンB12なども含まれています。卵を中心としたこの料理は、糖質も低く、低血糖を抑えながら長時間エネルギーを支えてくれるため、健康に良い料理といえます。

SequentialChainサンプル

from dotenv import load_dotenv
from langchain.chains import LLMChain, SequentialChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

load_dotenv()

llm = OpenAI(temperature=0.9)

template = """\
あなたは料理家です。国と食材を与えるので料理を考えてください

国:{country}
食材:{food}
料理:\
"""

prompt_template = PromptTemplate(input_variables=["country", "food"], template=template)

first_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="dish")


template = """\
あなたは栄養士です。料理を与えるので栄養価を教えて下さい

料理:{dish}
栄養価:\
"""

prompt_template = PromptTemplate(input_variables=["dish"], template=template)

second_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="value")

overall_chain = SequentialChain(
    chains=[first_chain, second_chain],
    input_variables=["country", "food"],
    output_variables=["dish", "value"],
    verbose=True,
)

review = overall_chain({"country": "フランス", "food": "鶏肉"})

print(review)

実行結果

Entering new SequentialChain chain...

Finished chain. {'country': 'フランス', 'food': '鶏肉', 'dish': '鶏肉のドリア', 'value': '鶏肉のドリアは、1カップあたりの熱量が390 kcal、たんぱく質が全21.2 g、脂質が全7.3 g、炭水化物が全51.1 gであると言われています。しかしながら、栄養バランスを考慮した上で、食材や調理方法などによって栄養価が変動しうることを忘れないでください。'}

分かったこと

  • SimpleSequentialChainは後続チェーンに与える値が1つなので各々のチェンでinputの値のみ指定すれば良く、outputの値を指定しなくて良い
  • SimpleSequentialChainはrunコマンドでワードを与えて実行する
  • SequentialChainは後続のチェーンに値を渡すためにoutput_keyを指定する必要がある。
  • SequentialChainはコンストラクタにワードを与えて実行する

langchainのchainとagentの連携について

langchainの勉強の一貫としてchainとagentの連携を行ってみた。

フォルダ構成

langchain_work_3
├── agent.py
├── main.py
├── output_parser.py
└── tools.py

処理内容

  1. main.pyからagent.pyを呼び出す
  2. agent.pyの中で条件を達成するためのツールをtools.pyから選択。tools.pyを使用し必要な値を取得
  3. 2にて取得した値をmain.pyの中でパースし、json形式に変換

※今回tools.pyは日本株価ニュースとアメリカ株価ニュースを文字列で返却
※main.pyからagent.pyを呼び出す際の引数(日本 or アメリカ)で自動的にagent側で使用するツールを判別する

処理結果

Entering new AgentExecutor chain... I need to provide information on Japanese stock market Action: 日本の株式情報 Action Input: None Observation: 日経平均は大幅に3日続伸。2日の米株式市場でダウ平均は701.19ドル高と大幅続伸。財政責任法案が上院で可決、債務不履行(デフォルト)が回避されたことで買いが先行。5月雇用統計は強弱入り混じる内容だったが、今月開催の連邦公開市場委員会(fomc)での利上げ一時停止の予想を変更させるほどの内容ではないとの見方から相場を一段と押し上げた。ナスダック総合指数は+1.06%と続伸。米株高を引き継いで日経平均は339.9円高からスタート。再び140円台に乗せた円安・ドル高や中国による経済政策期待も手伝い、景気敏感株を中心に買いが加速。心理的な節目を前にもみ合う場面もあったが、値がさ株やハイテク株にも買いが入るなか、前場中ごろには32000円を突破。後場は一段と上値を伸ばす展開となり、高値引けとなった。

大引けの日経平均は前日比693.21円高の32217.43円となった。東証プライム市場の売買高は14億7600万株、売買代金は3兆8712億円だった。セクターでは機械、海運、繊維製品が上昇率上位に並んだ一方、電気・ガスのみが下落した。東証プライム市場の値上がり銘柄は全体の89%、対して値下がり銘柄は9%だった。

Thought:I have the information on the Japanese stock market Final Answer: The Nikkei average rose significantly for the third consecutive day, with the Dow Jones Industrial Average rising significantly in the US stock market on the second day. The market was driven by buying after the Fiscal Responsibility Bill was passed in the Senate, avoiding default. The May employment statistics were mixed, but not enough to change expectations of a temporary pause in interest rate hikes at the Federal Open Market Committee (FOMC) meeting this month, pushing the market up further. The Nasdaq Composite Index also continued to rise. The Nikkei average started at 339.9 yen higher, riding on the yen's depreciation and the expectation of China's economic policy. While there were some struggles before the psychological milestone, buying accelerated, especially in cyclical stocks, and the market broke through 32,000 yen in the middle of the morning session. In the afternoon, the market continued to rise, and the closing price was at a high. The Nikkei average at the close was 32,217.43 yen, up 693.21 yen from the previous day. The trading volume on the Tokyo Stock Exchange Prime Market was 1.476 billion shares, with a trading value of 3.8712 trillion yen. In terms of sectors, machinery, shipping, and textile products were among the top gainers, while only electricity and gas declined. The number of rising stocks on the Tokyo Stock Exchange Prime Market was 89% of the total, while the number of declining stocks was 9%.

Finished chain. {"stock": "32,217.43 yen", "late": "693.21 yen"} 想定どおり、日本株の株価と上昇額が取れた

ポイント

  • initialize_agent()でagentインスタンスを作成する時、引数にverbose=Trueを付けると処理内容が詳細に分かりデバックしやすい
  • 処理結果をjson型にするなど指定したい場合はPromptTemplateのpartial_variablesに辞書型で{"format_instructions": info_output_parser.get_format_instructions()}と値を渡す。info_output_parserはpydanticのBaseModelを継承したクラスで型を定義しPydanticOutputParserで取得したインスタンス。

ソース

main.py

from agent import request_agent
from dotenv import load_dotenv
from langchain import PromptTemplate
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI
from output_parser import info_output_parser

load_dotenv()

today_news = request_agent("日本")

template = """
与えた情報から以下を出力して下さい。
{info}
- 株価
- 前日比
\n{format_instructions}
"""

sum_tmplate = PromptTemplate(
    input_variables=["info"],
    template=template,
    partial_variables={"format_instructions": info_output_parser.get_format_instructions()},
)

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo")

chain = LLMChain(llm=llm, prompt=sum_tmplate)


print(chain.run(info=today_news))

agent.py

from langchain import PromptTemplate
from langchain.agents import AgentType, Tool, initialize_agent
from langchain.chat_models import ChatOpenAI

from langchain_work_3.tools import get_japan_stockt, get_usa_stockt


def request_agent(word: str) -> str:
    template = """
    {word}の株式情報を教えて
    """

    prompt_template = PromptTemplate(template=template, input_variables=["word"])

    llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")

    tools_for_agent = [
        Tool(
            name="日本の株式情報",
            func=get_japan_stockt,
            description="日本の株式情報をテキストメッセージで返却します",
        ),
        Tool(
            name="アメリカの株式情報",
            func=get_usa_stockt,
            description="アメリカの株式情報をテキストメッセージで返却します",
        ),
    ]

    agent = initialize_agent(tools=tools_for_agent, llm=llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
    return agent.run(prompt_template.format_prompt(word=word))

tools.py

def get_japan_stockt(arg) -> str:
    return """\
日経平均は大幅に3日続伸。2日の米株式市場でダウ平均は701.19ドル高と大幅続伸。財政責任法案が上院で可決、債務不履行(デフォルト)が回避されたことで買いが先行。5月雇用統計は強弱入り混じる内容だったが、今月開催の連邦公開市場委員会(fomc)での利上げ一時停止の予想を変更させるほどの内容ではないとの見方から相場を一段と押し上げた。ナスダック総合指数は+1.06%と続伸。米株高を引き継いで日経平均は339.9円高からスタート。再び140円台に乗せた円安・ドル高や中国による経済政策期待も手伝い、景気敏感株を中心に買いが加速。心理的な節目を前にもみ合う場面もあったが、値がさ株やハイテク株にも買いが入るなか、前場中ごろには32000円を突破。後場は一段と上値を伸ばす展開となり、高値引けとなった。

大引けの日経平均は前日比693.21円高の32217.43円となった。東証プライム市場の売買高は14億7600万株、売買代金は3兆8712億円だった。セクターでは機械、海運、繊維製品が上昇率上位に並んだ一方、電気・ガスのみが下落した。東証プライム市場の値上がり銘柄は全体の89%、対して値下がり銘柄は9%だった。
"""


def get_usa_stockt(arg) -> str:
    return """\
午後3時過ぎにS&P500は前日比+1.65%の4290まで上昇し、高値圏で終了した。ダウ平均が+2.12%、S&P500が+1.45%、ナスダック総合が+1.07%。全11セクターが上昇し、中でも素材が+3.37%、資本財エネルギーが共に+2.96%。個別では、3M(MMM)が水質汚染問題で複数都市と暫定和解に達し+8.75%。ルルレモン・アスレティカ(LULU)が好調なF1Q決算を発表し+11.3%。モンゴーDB(MDB)がF1Q業績の大幅上振れを示し+28.01%。他方、TモバイルUS(TMUS)が上記アマゾン(AMZN)の参入発表を受け-5.56%。センチネルワン(S)がF1Q決算にて実績・ガイダンスが嫌気され-35.14%
"""

output_parser.py

from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field


class InfoOutput(BaseModel):
    stock: str = Field(description="stock value")
    late: str = Field(description="stock late today")

    def to_dict(self):
        return {"stock": self.stock, "late": self.late}


info_output_parser: PydanticOutputParser = PydanticOutputParser(pydantic_object=InfoOutput)

投資タイミングを逃した

NVIDIAの決算が想定以上に良かったこともあり、株価全体が順調に推移している。自分はほぼ現金で待機させていたこともあり今回の波に乗る事ができなかった。 非常に悲しいが投資はターン制だと思っているので、今から無理に入ることは無いが今後のために教訓を残しておきたい。

1. 新しい名詞が出来て来たら注意する

今回NVIDAの決算が良かった理由はChatGPTを筆頭にAI投資に各社が熱を入れていることが理由にあげられる。以前もコロナ禍でzoomが流行った時は爆発的な株価上昇があった。 このように新しい名詞が生まれたタイミングで関連する株は何かを常に考え一歩先行く投資を行うことが利益をあげるためには大切だと考える。

2.株は全体が下げている時に仕込む

アメリカの金利引き上げに伴い、株パフォーマンスは全体的に悪かった。株を仕込むのであればこのようなタイミングで仕込む必要がある。

3.相場が分からない時はインデックスを買う

現在のような相場の上げ下げが分かりづらい時はインデックスを買って無難に凌ぐ。金融緩和などの上昇が見込める局面では積極的に個別に投資する。

4.VIXは現在の数字よりも変化率を重要視する

現在のVIXは18%程度と極端に低い。このままでは指標として役に立たないので上昇率を意識する。8%ぐらい一日に上昇した場合は良い投資タイミングとなることが多い。