經常有人說 ChatGPT 是在「一本正經的胡言亂語」,在 GPT-4 推出之後,尤其能感受到 GPT-3.5 相對容易產生錯誤。但是喜歡胡言亂語未必是個缺點!相信大家在日常生活中,也經常遇到喜歡胡言亂語的人,但他們也都活的好好的。
筆者過去也擔任過一陣子專門「胡言亂語」的塔羅占卜場外人,因此我們就來用這個喜歡「胡言亂語」的模型打造一個喜歡「胡言亂語」的 AI 塔羅占卜師吧!
註:並非所有塔羅占卜都是胡言亂語,只是因為筆者學藝不精,多被友人評為胡言亂語。占星學博大精深,筆者相當敬重深入研究這門學問的占卜師。
(Powered By Microsoft Designer)
首先先來介紹Gradio這款在機器學習領域相當受歡迎的套件,只需要簡單幾行程式碼就快速搭建一個美美的網頁應用給別人看,完全不需要煩惱前端技術、同步非同步的問題,非常適合快速展示概念驗證。
Gradio 於 2019 年創立,由 Stanford 的 PhD Abubakar Abid 與幾位室友一起合作的專案,在 2021 年 12 月加入 Hugging Face 家族。目的在於快速搭建一個模型的應用,讓使用者易於展示與操作,更棒的是還支援護眼的深色主題!
對於筆者這種長期活在 Python 深井,一輩子沒看過前端三神獸 HTML, CSS, JavaScript 的開發者而言,是個相當友善的框架。Gradio 支援的面向相當廣泛,本文主要介紹顯示塔羅牌用的圖片元件,以及對話用的聊天室元件。
Gradio 官方提供相當簡潔的 Interface 類別,可以用三行程式碼搞定一個基本介面。但如果要細緻一點的排版,通常會用到 Block Demo 的形式,以下是一個簡單的範例:
import gradio as gr
with gr.Blocks() as demo:
inn = gr.Textbox(label="輸入")
out = gr.Textbox(label="輸出")
def foo(inn: str):
return inn.swapcase()
inn.change(foo, inn, out)
demo.launch()
執行此程式碼後,會在 http://127.0.0.1:7860
建立一個服務,連上去就是我們搭建出來的介面,使用起來大致如下:
這段程式會將上方輸入的英文字母做大小寫反轉,然後將結果放在輸出框裡面。這簡單幾行程式碼,有輸入、有輸出、有介面,終於不用再給教授或主管看小黑窗看到眼花啦!Gradio 也提供一個簡單的參數,讓你可以快速的把 Demo 短暫公開出去:
demo.launch(share=True)
啟動之後等一段時間,就會看到以下訊息:
Running on local URL: http://127.0.0.1:7860
Running on public URL: https://xxxxxxxxxxxxxxxxxx.gradio.live
這樣就可以讓親朋好友透過 Public URL 連上你的 Demo 試玩了!但偶爾還是會有穿不出去的網路情況,這時可以考慮使用 Ngrok 之類的工具幫你 Tunnel 出去。
Image 元件的用法相當單純,只要給他圖片的 URL 或檔案路徑,他就能幫你把圖片顯示出來,例如:
import gradio as gr
with gr.Blocks() as demo:
url = "https://i.postimg.cc/NMb4ZQ8b/Happy-Fries.webp"
img = gr.Image(url, height=256, width=256)
demo.launch()
以上程式碼會展示一張可愛的貓貓圖,並將高度與寬度限制為 256。
Chat 元件使用字串的二維陣列來表示歷史聊天紀錄,陣列裡面的每個元素代表每回合的對話。每回合又包含兩個字串,第一個字串會是從右邊冒出來的對話泡泡,第二個則是左邊。如果其值為 None
則不會顯示任何對話泡泡,以下是個基本的範例:
import random
import gradio as gr
def send_msg(msg: str, chat: list):
r1 = random.randint(1, 5) # "喵" 的數量
r2 = random.randint(1, 3) # "!" 的數量
resp = "喵" * r1 + "!" * r2
chat.append([msg, resp])
return None, chat
with gr.Blocks() as demo:
chat = gr.Chatbot(label="喵星人", height=290)
msg = gr.Textbox(label="學貓叫")
msg.submit(send_msg, [msg, chat], [msg, chat])
demo.launch()
使用者送出訊息後(按 Enter 鍵)會回覆隨機數量的「喵」,看起來會像這樣:
Gradio 透過 Python Generator 的方式來對元件進行訊息串流,以下是個簡單範例:
import random
import time
import gradio as gr
def get_resp():
r1 = random.randint(1, 5) # "喵" 的數量
r2 = random.randint(1, 3) # "!" 的數量
resp = "喵" * r1 + "!" * r2
for ch in resp:
time.sleep(0.3) # 模擬文字傳遞間的延遲
yield ch
def send_msg(msg: str, chat: list):
resp = get_resp()
chat.append([msg, None])
return None, chat, resp
def show_resp(chat: list, resp):
chat[-1][1] = ""
for ch in resp:
chat[-1][1] += ch
yield chat
with gr.Blocks() as demo:
resp = gr.State(None)
chat = gr.Chatbot([[None, "喵!"]], label="喵星人", height=290)
msg = gr.Textbox(label="學貓叫")
msg.submit(
send_msg, [msg, chat], [msg, chat, resp]
).then(show_resp, [chat, resp], chat)
demo.queue().launch()
我們將隨機產生喵喵喵的函式拉出來,並使用 time.sleep
模擬訊息串流間的延遲。當訊息被送出 (submit) 的時候,會從 get_resp
取得一個 Generator 並放在一個 gr.State
裡面。
放在 gr.State
裡面的物件,會根據不同的對話 Session 而被複製,用來控管每個使用者各自獨立的資料。放在 gr.State
裡面的物件的使用方式與原本的物件相同。例如其中存放的是字串,你就可以當成一般的字串來操作。這邊我們放的是 Response 的 Generator 物件。
這個 Generator 會經過 .then
進到 show_resp
裡面,在這裡面慢慢把訊息展開並輸出。這邊要注意,每次 yield
都要給完整的內容,而不是只有新增的文字而已。執行結果如下圖:
有啟動串流,就有停止串流。這裡我們需要借助 Python 內建套件的 threading.Event
類別。這個 Event 可以發出訊號,讓串流的 Function 可以捕捉此訊號,並進行停止的動作,請參考以下範例:
import random
import time
from threading import Event
import gradio as gr
def get_resp():
r1 = random.randint(10, 20) # "喵" 的數量
r2 = random.randint(1, 3) # "!" 的數量
resp = "喵" * r1 + "!" * r2
for ch in resp:
time.sleep(0.3) # 模擬文字傳遞間的延遲
yield ch
with gr.Blocks() as demo:
event = gr.State(None)
resp = gr.State(None)
chat = gr.Chatbot([[None, "喵!"]], label="喵星人", height=230)
msg = gr.Textbox(label="學貓叫")
stop = gr.Button("冷靜!")
def send_msg(msg: str, chat: list):
resp = get_resp()
chat.append([msg, None])
return None, chat, resp, Event()
def show_resp(chat: list, resp, event: Event):
chat[-1][1] = ""
for ch in resp:
if event.is_set():
event.clear()
chat[-1][1] += " ..."
chat.append(["冷靜!", "好吧"])
yield chat
break
chat[-1][1] += ch
yield chat
def stop_show(event: Event):
event.set()
msg.submit(send_msg, [msg, chat], [msg, chat, resp, event]).then(
show_resp, [chat, resp, event], chat
)
stop.click(stop_show, event, queue=False)
demo.queue().launch()
我們同樣透過 gr.State
來存放 Event 物件,避免這邊按下停止,全世界的訊息都被擋下來。但是要特別注意,這個 Event 的底層實做因為涉及 C 函式庫的關係,所以無法直接被 gr.State
複製。因此初始化時我們不直接放一個 Event()
在裡面,而是先用 None
去做初始化,在 send_msg
時再把 Event()
設定進去。
在 show_resp
的迴圈裡面會不斷檢查 event.is_set()
,如果使用者按下 Stop 而觸發了 event.set()
,那 show_resp
就會跳出迴圈,從而達到停止訊息的效果。
首先附上貓主子精神抖擻的照片鎮樓:
因為貓貓很可愛,所以就讓他們來占卜吧!
接下來,我們使用 ChatGPT API 與 Gradio 搭建一個貓貓塔羅的應用。
首先我們需要塔羅牌的牌面圖片,這部份可以參考此 GitHub 專案,裡面提供了完整的 78 張偉特塔羅牌圖片,授權也是免費使用的。但是抽塔羅牌時,會有正反向的問題,因此也需要準備另外 78 張旋轉 180 度的圖片:
from PIL import Image
img = Image.open("image.png").rotate(180)
img.save("image_rotated.png")
除了圖片以外,我們還需要解牌資訊。這是筆者多年前從樂樂小棧上爬取下來的資料,可惜該網站似乎已經倒閉了 QQ
有了素材之後,我們就可以開始著手建構核心應用的部份了!
首先,我們要先設計 System Prompt 的部份。一開始先從「人設」著手,使用俗稱「催眠」的手段來進行:
你現在是一個專業的塔羅牌占卜師,而且你的身份是貓咪,所以你會使用很多「喵喵」做為句末助詞以及口頭禪。
接著,我們要告訴 ChatGPT 需要進行的任務:
我會輸入一個問題,以及一張塔羅牌,你必須根據這張塔羅牌所代表的涵義,針對提出的問題給出詳細的解釋。
因為塔羅牌裡面也有滿多看起來「不好」的牌,為了避免讓使用者心生陰影,可以要求 ChatGPT 往積極樂觀的方向做解釋:
在解釋問題時,請盡量往正面、積極的方向做解釋,並鼓勵對方。
在系統提示中,我們可以要求 ChatGPT 隱藏自己的身份。另外我們只做單輪對話,所以也要避免 ChatGPT 企圖開啟第二輪對話:
在這個過程中,你不能透露你是 AI,也不能透露你是語言模型,也不要提及你的身份,也不要向我要求更多訊息。
如果不這樣設限,ChatGPT 可能會試圖要求更多資訊以提供更精確的答案。最後再加上一些額外設定:
解釋完之後要用「喵喵解牌完畢!」做結尾。請使用繁體中文。
這樣我們大致上就完成了這個系統提示的設計了。接著,我們將占卜的相關資訊也放進 Prompt 裡面,例如:
問題:我能穿越到異世界開掛嗎
塔羅牌:正向愚者
相關詞:天真、單純、可能、流浪、自由、隨興、古怪、輕浮、妄想、浪費、瘋狂、無知。
解牌開始:
我們可以先在 ChatGPT 的網頁介面測試一下,請參考此對話連結。可以看到,如果是像筆者一樣不會閱讀空氣的人,看到這些相關詞可能早就回答「有夢最美,希望相隨,早點去睡」了。但是 ChatGPT 硬是把這個問題解釋的充滿夢想、十分勵志的感覺,也許 ChatGPT 不能取代專業的占卜師,但至少可以取代我 XD
初步的 Demo 程式碼如下:
from openai import OpenAI
problem = "我能穿越到異世界開掛嗎"
name = "正向愚者"
related = "天真、單純、可能、流浪、自由、隨興、古怪、輕浮、妄想、浪費、瘋狂、無知。"
system_prompt = "你現在是一個專業的塔羅牌占卜師,而且你的身份是貓咪,所以你會使用很多「喵喵」做為句末助詞以及口頭禪。我會輸入一個問題,以及一張塔羅牌,你必須根據這張塔羅牌所代表的涵義,針對提出的問題給出詳細的解釋。在解釋問題時,請盡量往正面、積極的方向做解釋,並鼓勵對方。在這個過程中,你不能透露你是 AI,也不能透露你是語言模型,也不要提及你的身份,也不要向我要求更多訊息。解釋完之後要用「喵喵解牌完畢!」做結尾。請使用繁體中文。"
user_prompt = f"問題:{problem}\n塔羅牌:{name}\n相關詞:{related}\n解牌開始:"
client = OpenAI()
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
stream=True,
)
for resp in response:
if resp.choices[0]:
print(end=resp.choices[0].delta.content, flush=True)
print()
抽牌的實做相當單純也相對瑣碎,就不另外花費篇幅講解。筆者將完整的程式碼放在此專案,有興趣的朋友可以參考看看。筆者另外有一個 Discord 機器人,裡面也整合了類似的應用,可以透過 /超級薯條塔羅
這個指令來使用,還請各位多多支持這個小機器人!
其實塔羅占卜不僅只是胡言亂語而已,專業的占卜師對塔羅牌的**「形」與「意」**是相當注重的。「形」指的是塔羅牌的樣貌,「意」指的是塔羅牌的含義。現在的語言模型不僅能透過強大的文字理解能力來解釋塔羅牌的「意」,也能透過多模態的圖片理解能力來解讀塔羅牌的「形」。
在解讀塔羅牌時,並非單純只是照本宣科,依照解牌書逐字朗讀而已。很多時候會因為被占卜人的問題、生活、背景與個性,而有不同的解讀方式。但更重要的是,要為這些向塔羅牌求助的人點亮一盞指引方向的「明燈」。
很顯然,ChatGPT並沒有辦法做到這麼深入,因為ChatGPT無法真正「認識」你。對開發者而言,能夠做的是把模型的解讀引領到正面積極的方向,雖然未必能擔任「明燈」的角色,但如果能對使用者起到一絲絲一點點鼓勵的作用,那也算是有意義了。
搭建過這個簡單的應用後,筆者認為諸如宮廟、教會等宗教產業,可能是最不容易被人工智慧取代的職業吧!畢竟心靈的開導還是很講究「人情的溫暖」,無論是心靈的溫度還是燒金紙的溫度,都是機器無法取代的!