はじめに
いろんな年収の独身男性100名(ChatGPT)に、家賃が高い都心の物件か、家賃が低い郊外の物件どちらに住みたいか聞いてみます。
以前Twitterに、GPT-3を市場調査に使うという論文が流れてきました。なんか面白そうなのでやってみようと思います。
3行まとめ
- 都心か郊外、どっちにすみたいか大勢の独身男性になりきったChatGPTにインタビューしてみたよ。
- 順番に聞いてくと時間がかかるから、langchainを使って並列に聞いたよ。
- いい感じの結果がでたけど、そのためにはプロンプトをかなり調整する必要があるよ。
手順
01.回答者の設定と質問文を作る
まずはChatGPTに独身男性になりきってもらうためのプロンプトを作成します。質問文では2つの物件のうちどちらに住みたいかを記載します。
いい感じの結果を得るポイントは、年収と家賃の比較手順をプロンプトに書くことです。また「通勤時間は短い方が良い」など回答者の価値観も書いておいた方が良いと思います。
論文ではtext-davinci-003
を使っていますが、料金が高いでgpt-3.5-turobo
を使います。回答のばらつきを大きくするためtemperature=1
とします。また回答は選択肢の順番にも影響を受けるらしく、ちゃんとやるならランダムに順序を変えるなど工夫が必要なようです。論文のプロンプトはこれよりもシンプルなので、ほんとにこれでいい感じの結果が得られるのか?と思ったのですが、質問の仕方とかかなり注意を払っているようです。
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
chat = ChatOpenAI(model_name='gpt-3.5-turbo',temperature=1)
system_template="""
あなたは年収{annual_income}万円の独身の男性です。
趣味はゲームで、休日は家でゲームをして過ごすことが多いです。
食事は外食が多く、月に5万円ほどかかっています。
その他に交通費や交際費で月数万はかかってます。
現在は新宿駅の近くに勤務しています。
2択での質問がされます。
回答例に従ってJSON形式で回答してください。
理由では、まず年間でいくら支払うことになるか計算してください。
年収の4分の1程度は税金などで引かれるため、まず1年間で自由に使用できる金額を計算してください。
次に1年間あたりの家賃を計算してください。
その後1年間で自由に使える金額と家賃を比較して、余裕があるかどうかを判断してください。
一般的に家賃は自由に使用できる金額の3分の1から4分の1が適切と言われています。
食費や趣味などに使用する金額もなるべく残しておく必要があります。
つぎに通勤時間について考えてください。
通勤時間はできれば短い方が良いです。
貯金もたくさんした方がよいです。
そのあとこれまで考えたことをまとめて、どちらに住むか決めてください。
理由には改行を入れないでください。
#解答例
{{'answer':'都心', 'reason':''}}
{{'answer':'郊外', 'reason':''}}
"""
system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)
human_template=f"""
以下のふたつのマンションに住むならばどちらに住みますか?
都心:家賃が月20万円の新宿駅徒歩圏内で通勤時間5分の都心のマンション
郊外:家賃が月8万円の新宿まで通勤時間が40分かかる郊外のマンション
"""
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])
chain = LLMChain(llm=chat, prompt=chat_prompt)
ためしに年収400万から1200万の5名の方に聞いてみましょう。
for annual_income in [400, 600, 800, 1000, 1200]:
print(annual_income, chain.run(annual_income=annual_income))
# > 400 {'answer': '郊外', 'reason': '年収400万円だと、家賃20万円だと生活が苦しくなります。また、都心の家賃は通常の物件価格より高く設定されているケースが多く、生活費も高くなります。一方、郊外の方が家賃が割安で、交通費も通勤時間が長くなる分多少かかっても、全体的に生活費を削減できます。'}
# > 600 {'answer':'郊外', 'reason':'年収600万円のうち、家賃として月20万円を支払っていたら、年間収入から240万円減ることになります。年収から税金や社会保険料を引いた自由に使用できる金額は、約380万円になります。家賃以外にも交通費や交際費がかかっているため、1年間で出費が大きくなります。都心のマンションに住んだ場合、通勤時間は短くなりますが、家賃が高すぎるため、その分趣味や食費などの余裕がなくなってしまいます。一方、郊外のマンションに住めば、通勤時間は長くなりますが、家賃が安いため、余裕が出ます。移動時間には本を読んだり、スマートフォンで遊んだりすることができます。家賃は通常の収入の3分の1から4分の1が妥当と言われていますが、都心の家賃がその倍以上なので、慎重に判断する必要があります。'}
# > 800 {'answer':'郊外', 'reason': '年収800万円から、税金や社会保険料等を除いた手取りの額は年間約600万円です。そのうち家賃に3分の1(200,000円)を使ってしまう都心では、交際費や趣味に割ける予算が大幅に減ってしまいます。一方で、郊外では家賃が月8万円と、自由に使える金額の範囲内で家賃を抑えることができます。また、通勤時間は長くなりますが、交通費が節約できます。通勤時間だけでなく、生活の余裕や趣味にも余裕をもって充実させたいということから、郊外のマンションが適しています。'}
# > 1000 {'answer':'都心', 'reason':'年収1000万円ということで、家賃20万円は無理なく払える範囲です。また、通勤時間が5分と短くストレスが少なくなるため選びました。交通費も削減できるので、自由に使える金額も多くなります。'}
# > 1200 {'answer':'都心', 'reason':'年収1200万円であり、交通費や交際費で数万円かかっているため、余裕がある程度あると考えられる。都心のマンションは通勤時間が短く、趣味であるゲームをする時間を確保できるため居住地として好ましいと判断。家賃が比較的高いが、都心の居住地としては相応な価格帯であると考えられる。'}
ふむふむ、それっぽい感じですね。
といっても、いい感じになるまでプロンプトを数10回いじりまくったので、当たり前ですが・・・。プロンプトに通勤時間はできるだけ短い方が良いと書くと無理してでも都心選びがちになりますし、書かないと高年収でも郊外選びがちですし、プロンプトの書き方次第ですね。
ちなみに、実行時間は1分30秒程度です。
たった5名に聞いただけで90秒もかかっていては100名以上に聞くには時間がかかりすぎます。そこでLangchainを使って並列に聞いていきます。
02.並列にAPIを使う
以下のサイトを参考にして、並列に実行していきます。
import asyncio
async def async_generate(chain, annual_income):
resp = await chain.arun(annual_income=annual_income)
print(annual_income, resp)
return annual_income, resp
async def generate_concurrently(chain, annual_income_list=[600, 800],n=5):
tasks = [
async_generate(chain, annual_income)
for annual_income in annual_income_list
for _ in range(n)
]
return await asyncio.gather(*tasks)
年収400万から1200万をもつ独身男性、各20名・合計100名に非同期で聞いていきます。
outs = await generate_concurrently(chain, annual_income_list=[400, 600, 800, 1000, 1200], n=20)
出力の順番はバラバラです。回答が終わった順に、理由が短いものから出力されています。逆に一番理由が長いものはこれでした。計算ミスしてますがあれこれ考えてますね。
{
'answer':'郊外',
'reason':'まず年収が1,400万円なので、税金などで約350万円引かれることを考慮して、
年間で約825万円が自由に使用できる金額と仮定します。月に食事代が5万円かかるため年間で60万円、
交通費や交際費で月に2万円だとすると年間で24万円がかかると仮定すると、合計で年間84万円がかかります。
家賃は都心が月20万円で年間240万円、郊外の場合は月8万円で年間96万円です。
家賃を除いた残りの自由に使える金額は、都心では585万円、郊外では759万円あります。
それぞれ家賃を加えた場合、都心では825万円-240万円=585万円、郊外では825万円-96万円=729万円になります。
このように計算すると、都心では余裕がなくなっていますが、郊外ではまだ余裕があります。
また、都心のマンションは通勤時間が5分ととても短いですが、同じくらいの広さの住居を郊外で見つけることができれば、
通勤時間が40分で半額以下の家賃で暮らすことができます。
そのため、家賃と自由に使える金額のバランスを考えると、郊外のマンションに住む方がベストだと思います。'
}
03.結果
最後に出力を整えて結果を可視化してみます。
100件中98件はjson形式として読み取れました。読み取れなかったものは除外します。
"""
出力整理
"""
import pandas as pd
records=[]
for annual_income, dicstr in outs:
try:
dic = eval(dicstr.replace('\n',''))
dic['annual_income'] = annual_income
records.append(dic)
except:
print(annual_income)
continue
data = pd.DataFrame.from_records(records)
#> 400
#> 1200
"""
可視化
"""
import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib
plot_data=(
data.assign(n=1)
.groupby(['annual_income', 'answer'])['n'].sum().reset_index()
.assign(rate = lambda df: df.groupby('annual_income')['n'].transform(lambda s: s/s.sum())*100)
.set_index(['annual_income', 'answer'])['rate'].unstack().fillna(0)
)
plt.figure(figsize=(10, 5))
plot_data.plot(
kind='bar', stacked=True,
title='ChatGPTに聞いた年収別の家賃20万都心と家賃8万郊外の選択割合',
rot=0, fontsize=15, width=0.8, edgecolor='black',
linewidth=2, xlabel='年収', ylabel='割合(%)',
)
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0, fontsize=15)
plt.show()
グラフを見ると、年収が上がるほど都心を選択している人が多くなっています。個人的にいい感じに動いたなと思った点は、年収600万以下で家賃20万の物件を選択するといった無理のある選択をしていない点です。
追記
選択肢の順番を入れ替えた場合と、独身男性独身女性のそれぞれの場合で実行してみました。
左上の(男性、選択肢の先頭が家賃20万都心)の図は、先ほど実行した条件と全く同じものです。男女差はよくわからないですが、選択肢の順序の影響はありそうですね。
選択肢の先頭が家賃20万都心 | 選択肢の先頭が家賃8万の郊外 | |
---|---|---|
男性 | ||
女性 |
まとめ・感想
langchainを並列に動かして、ChatGPTにインタビューしました。
非同期実行のやり方を調べるための題材としてやりましたが、一連の処理を作ってみると、もっと工夫しないといけない点がありますね。たとえばChainを動かすたびに選択肢の順番をランダムに入れかえる方法や、結果を安定させる方法など。
LLMにインタビューするというのも面白い手法だと思いました。いろんな商品の価格弾力性なども再現できそうです。ただプロンプトの書き方で結果は大きく左右するので、使い道を探したいですね。