iT邦幫忙

2023 iThome 鐵人賽

DAY 5
4

簡介

經常有人說 ChatGPT 是個喜歡「一本正經胡言亂語」的東西,也許吧!在 GPT-4 推出之後,與 GPT-3.5 相比起來,確實相對容易產生錯誤。但是經常胡言亂語的模型難道就不能用了嗎?那恐怕也未必,相信大家在日常生活中,也經常遇到喜歡胡言亂語的人,但他們也都活的好好的。在筆者過去的生活經歷裡,也擔任過一陣子專門「胡言亂語」的塔羅占卜場外人,因此我們就來用這個喜歡「胡言亂語」的模型打造一個喜歡「胡言亂語」的 AI 占卜師吧!

註:並非所有塔羅占卜都是胡言亂語,只是因為筆者學藝不精,多被友人評為胡言亂語。占星學博大精深,筆者相當敬重深入研究這門學問的占卜師。

可愛貓貓 Day 5

(Powered By Microsoft Designer)

今天會先介紹 Gradio 這款在機器學習領域相當受歡迎的套件,可以用簡單幾行程式碼快速搭建一個有 GUI 的網頁 Demo 給別人看,完全不需要煩惱同步非同步的問題。接著搭配 Gradio 套件結合 ChatGPT API 來搭建一個完整的貓貓塔羅占卜應用。

Gradio

Gradio 於 2019 年創立,由 Stanford 的 PhD Abubakar Abid 與幾位室友一起合作的專案,在 2021 年 12 月加入 Hugging Face 家族。目的在於快速搭建一個模型的應用,讓使用者易於展示與操作,更棒的是還支援護眼的深色主題!

對於筆者這種長期活在 Python 深井,一輩子沒看過前端三神獸 HTML, CSS, JavaScript 的開發者而言,是個相當友善的框架。Gradio 支援的面向相當廣泛,本文主要介紹顯示塔羅牌用的圖片元件,以及對話用的聊天室元件。

gr.Blocks 基本用法

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 建立一個服務,連上去就是我們搭建出來的介面,使用起來大致如下:

Demo01

這段程式會將上方輸入的英文字母做大小寫反轉,然後將結果放在輸出框裡面。這簡單幾行程式碼,有輸入、有輸出、有介面,終於不用再給教授或主管看小黑窗看到眼花啦!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 出去。

gr.Image 圖片元件

Image 元件的用法相當單純,只要給他圖片的 URL 或檔案路徑,他就能幫你把圖片顯示出來,例如:

with gr.Blocks() as demo:
    url = "https://i.imgur.com/4IWxkgs.png"
    img = gr.Image(url, height=300)

demo.launch()

以上程式碼會展示一張「愚者」的塔羅牌。因為原圖解析度很高,所以限制高度為 300。

gr.Chat 聊天室元件

基本用法

Chat 元件使用字串的二維陣列來表示歷史聊天紀錄,陣列裡面的每個元素代表每回合的對話。每回合又包含兩個字串,第一個字串會是從右邊冒出來的對話泡泡,第二個則是左邊。如果其值為 None 則不會顯示任何對話泡泡,以下是個基本的範例:

import random

import gradio as gr

with gr.Blocks() as demo:
    chat = gr.Chatbot(label="喵星人", height=290)
    msg = gr.Textbox(label="學貓叫")

    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

    msg.submit(send_msg, [msg, chat], [msg, chat])

demo.launch()

使用者送出訊息後(按 Enter 鍵)會回覆隨機數量的「喵」,看起來會像這樣:

Demo

串流訊息

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


with gr.Blocks() as demo:
    resp = gr.State(None)
    chat = gr.Chatbot([[None, "喵!"]], label="喵星人", height=290)
    msg = gr.Textbox(label="學貓叫")

    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

    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 都要給完整的內容,而不是只有新增的文字而已。執行結果如下圖:

Demo

停止串流

有啟動串流,就有停止串流。這裡我們需要借助 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 就會跳出迴圈,從而達到停止訊息的效果。

Demo

貓貓塔羅

首先附上貓主子精神抖擻的照片鎮樓:

Fries

因為貓貓很可愛,所以就讓他們來占卜吧!

接下來,我們使用 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 程式碼如下:

import openai

openai.api_key_path = "API.Key"

problem = "我能穿越到異世界開掛嗎"
name = "正向愚者"
related = "天真、單純、可能、流浪、自由、隨興、古怪、輕浮、妄想、浪費、瘋狂、無知。"

system_prompt = "你現在是一個專業的塔羅牌占卜師,而且你的身份是貓咪,所以你會使用很多「喵喵」做為句末助詞以及口頭禪。我會輸入一個問題,以及一張塔羅牌,你必須根據這張塔羅牌所代表的涵義,針對提出的問題給出詳細的解釋。在解釋問題時,請盡量往正面、積極的方向做解釋,並鼓勵對方。在這個過程中,你不能透露你是 AI,也不能透露你是語言模型,也不要提及你的身份,也不要向我要求更多訊息。解釋完之後要用「喵喵解牌完畢!」做結尾。請使用繁體中文。"
user_prompt = f"問題:{problem}\n塔羅牌:{name}\n相關詞:{related}\n解牌開始:"

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ],
    stream=True,
)

for resp in response:
    try:
        print(end=resp["choices"][0]["delta"]["content"], flush=True)
    except:
        pass
print()

完整應用

抽牌的實做相當單純也相對瑣碎,就不另外花費篇幅講解。筆者將完整的程式碼放在此專案,有興趣的朋友可以參考看看。筆者另外有一個 Discord 機器人,裡面也整合了類似的應用,可以透過 /超級薯條塔羅 這個指令來使用,還請各位多多支持這個小機器人!

結論

其實塔羅占卜不僅只是胡言亂語而已,對塔羅牌的「形」與「意」是相當注重的。「形」指的是塔羅牌的樣貌,「意」指的是塔羅牌的含義。雖然 GPT-4 的論文宣稱能夠解讀圖片,但目前尚未開放透過 API 存取此功能,所以我們無法解讀塔羅牌「形」的部分。

而塔羅牌「意」的部分,也並非單純只是照本宣科,依照解牌書逐字朗讀而已。很多時候會因為被占卜人的問題、生活、背景與個性,而有不同的解讀方式。但更重要的是,為這些向塔羅牌求助的人點亮一盞指引方向的「明燈」。

很顯然,ChatGPT 並沒有辦法做到這麼深入,因為 ChatGPT 沒有辦法真正「認識」你。對開發者而言,能夠做的是把模型的解讀引領到正面積極的方向,雖然未必能擔任「明燈」的角色,但如果能對使用者起到一絲絲一點點鼓勵的作用,那這個應用也算有意義了。

搭建過這個簡單的 Demo 後,筆者認為諸如宮廟、教會等宗教產業,可能是最不容易被人工智慧取代的職業吧!畢竟心靈的開導還是很講究「人情的溫暖」,無論是心靈的溫度還是燒金紙的溫度,都是機器無法取代的。

參考


上一篇
LLM Note Day 4 - OpenAI API
下一篇
LLM Note Day 6 - ChatGPT 的挑戰者們
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言