LLM 學算術

上週一位前同事貼了篇文章:

看一個YouTube 影片測試Gemini and Chatgpt, 一個簡單的邏輯問題,一瓶汽水一元,兩個空瓶可以換一瓶新的汽水,請問20元可以喝到幾瓶汽水,兩個都答錯,

原本以為寫程式是個邏輯能力的展現,但是GenAI 邏輯不好為何可以寫程式呢?

然後臉友們試了各個 LLM, 沒有一個可以正確解答這題. 我試的 copilot 一開始答錯, 算出 30 瓶也就罷了. 提示它這是個遞迴的問題, 它竟然回答:

你說得對,這確實是一個遞迴的問題。讓我們再次計算一下,…

總結來說,你可以喝到無限多瓶汽水,只要你不停地用兩個空瓶換取新的汽水。

希望這個解答對你有幫助!

那麼正解是什麼呢? 簡單的解法是使用 chain of thought (COT), 很像是建構式數學. 我們要按照人類的思路一步一步地教它. 雖然我下面的描述提到一點 Action, 而不是純 Reaseon, 但 RecAct [3] 才是終極版結合 reason 和 action 的提示方式.

  1. 首先 20 元可以買 20 瓶汽水.
  2. 喝完 20 瓶汽水得到 20 個空瓶, 可以換 10 瓶新的汽水.
  3. 喝完 10 瓶汽水又得到 10 個空瓶, 可以換 5 瓶汽水.
  4. 喝完 5 瓶汽水又得到 5 個空瓶, 可以換 2 瓶汽水, 奇數的汽水產生一個空瓶還沒有兌換.
  5. 喝完 2 瓶汽水又得到 2 個空瓶, 可以換 1 瓶汽水.
  6. 喝完 1 瓶汽水又得到 1 個空瓶, 加上還沒兌換的 1 個空瓶, 又可以換一瓶汽水.
  7. 喝完 1 瓶汽水又得到 1 個空瓶 沒有還沒兌換的空瓶, 不能再喝了.

However, Copilot 無法學會. 繼續堅持 30 瓶的答案. 這招對它無效. 但是 ChatGPT 學得會 20 元的例子. 但改問它 10 元, 30 元可以喝幾瓶就沒辦法了.

根據我的理解, LLM 不是 encoder only, decoder only, 就是 encoder + decoder. 無論哪一類模型, 都要把文字轉向量. 在向量空間中, 類似的詞都有類似的向量. 所以狗, 犬, 㹴的向量都彼此靠近, 和貓距離比較遠. 以 encoder 類 LLM 來說, training 時隨機遮掉句子中的一個詞, 讓 LLM 學習填空. 學得再不完美, 關鍵字也會是同義字.

但是數字 1,2,3,4… 就沒有相關性了. 它本身沒意義, 很多個 1 也比 1 個 9 大 (11111…111 > 9). 因此看到數學很容易胡說八道, 但背公式可以.

想要徹底解決 LLM 數學不好的問題, 是採用 program-aid Language (PAL) model [1]. 也就是說, 我們叫 LLM 寫個小程式, 把原理寫成註解, 計算寫成數學式. 把這整個當作 prompt, LLM 應該就能學會了.

我不打算暗示 LLM 怎麼寫出 Python script, 於是我試著這樣問:

一瓶汽水一元,兩個空瓶可以換一瓶新的汽水,請問20元可以喝到幾瓶汽水?

# 一瓶汽水一元

Buy_bottle = money / 1

# 喝汽水

Used_bottle = buy_bottle

#  假如 used_bottle != 0, 兩個空瓶可以換一瓶新的汽水

While  used_bottle >= 2, do the following calculation

If used_bottle %2 = 0, buy_bottle += use_bottle / 2, used_bottle = 0

Else if used_bottle %2 = 1,  buy_bottle += use_bottle / 2,  used_bottle = 1

Answer = buy_bottle

結果 ChatGPT 算出 45 瓶, 因為它換過的汽水瓶又拿去換. 反倒是 Copilot 比較好一點. 雖然功敗垂成, 但是好歹它有聽進去我的話了~~~ 當然我的提示也有容易誤解的地方, 當我想寫得更詳細, 它卻答得更差. 所以我只能保留這一版.

這個問題是一個經典的換瓶問題,可以通過迭代計算來解決。讓我們根據你提供的算法來計算一下:

  1. 首先,用20元可以買到20瓶汽水。
  2. 喝完這些汽水後,你會有20個空瓶。
  3. 根據算法,每兩個空瓶可以換一瓶新的汽水,所以你可以換10瓶新的汽水。
  4. 現在你又有10個空瓶,可以再換5瓶新的汽水。
  5. 接著你又有5個空瓶,可以換2瓶新的汽水,並且還剩下一個空瓶。
  6. 最後,你可以用剩下的3個空瓶(之前剩下的1個加上剛換的2瓶的空瓶)換1瓶新的汽水。

所以,總共可以喝到的汽水數量是:

20+10+5+2+1=38 瓶汽水。

因此,用20元可以喝到38瓶汽水。🥤

自己都說是經典了還這樣. 那麼還有別招嗎?

有的. 這個技術的總稱叫做 LangChain [2]. LangChain 是個模組化的架構, 讓 LLM 更容易和應用程式結合. 它可以有很多 agent, 每個 agent 有自己的 memory, tools, 和 prompt template. 可以針對複雜的 prompt 拆成不同的屬性, 各自用適合的 tool 去處理. 至於 tool 最後會做什麼事情, 不是預先設定好的, 而是根據 LLM 的輸出決定的.

既然 LangChain 有個 chain 字, 我就從網路上找個 agent 內部有 chain 的圖示意.

本圖取材自 https://www.langchain.cn/t/topic/35

再回溯 Chain of Thought (COT) 和另外一個 Tree of Thought (TOT), 前者也提到chain, 他們和 LangChain 又是甚麼關係呢? 它們都只是輔助 LLM 推理的方法, COT 就是 step by step, TOT 就是找不到方法是會換一條思路. 它們都只是強調推理 (reason) 的部分, 有 action 才會去執行指令, 做成可互動的應用程式 (ReAct) [3].

[REF]

  1. https://arxiv.org/abs/2211.10435
  2. https://www.langchain.com/
  3. https://react.dev/

Jinja 小註解

想去日本玩, 趁機了解一下神社 (Jinja). 不同於普通的神社, 這個神社是 Python 的一個程式庫, 現在已經有 Jinja 2.

Jinja 主要用在建立 template, 例如產生動態網頁. 可替換的字串會在 template 中用 {{}} 包起來. 引用時指定這個字串等於某筆資料就行了.

import pandas as pd
from jinja2 import Environment, FileSystemLoader

# Create a simple DataFrame
data = {
    'Name': ['John', 'Anna', 'Peter', 'Linda'],
    'Age': [28, 34, 29, 32],
    'City': ['New York', 'Paris', 'Berlin', 'London']
}
df = pd.DataFrame(data)

# Define a Jinja template
template = """
<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Age</th>
            <th>City</th>
        </tr>
    </thead>
    <tbody>
        {% for _, row in df.iterrows() %}
            <tr>
                <td>{{ row['Name'] }}</td>
                <td>{{ row['Age'] }}</td>
                <td>{{ row['City'] }}</td>
            </tr>
        {% endfor %}
    </tbody>
</table>
"""

# Create a Jinja environment
env = Environment(loader=FileSystemLoader('.'))
template = env.from_string(template)

# Render the template with the DataFrame
html_table = template.render(df=df)
print(html_table)

上面的 code 執行後得到一段 HTML. 用 browser 開啟就會顯示下面的表格.

NameAgeCity
John28New York
Anna34Paris
Peter29Berlin
Linda32London

當然, 如果這些資料要 hard code 在 Python 裡面就很 low 了. 我們應該是從一個隨時變動的資料庫中讀出他們, 然後靠著 Jinja 做出動態更新的 HTML 網頁.

另外我們可以在 in-context learning 時教導 LLM 要做的事. 像是給它一個範例. 下面的 code 使用了 DEFAULT_KEYWORD_EXTRACT_TEMPLATE_TMPL 這個 template. 中間 text (紅字) 和max_ keywords (藍字) 是每次可置換的.固定的部分在於 告訴 LLM 說只能輸出用 comma (,) 分開的關鍵字. 不包括 stop word (如標點符號, 介係詞之類的).

from jinja2 import Template

# Define the template
DEFAULT_KEYWORD_EXTRACT_TEMPLATE_TMPL = Template(
    "Some text is provided below. Given the text, extract up to {{ max_keywords }}"
    " keywords from the text. Avoid stopwords.\n"
    "---------------------\n"
    "{{ text }}\n"
    "---------------------\n"
    "Provide keywords in the following comma-separated format: 'KEYWORDS: <keywords>'\n"
)

# Generate the prompt
def generate_prompt(text, max_keywords=5):
    return DEFAULT_KEYWORD_EXTRACT_TEMPLATE_TMPL.render(text=text, max_keywords=max_keywords)

# Example usage
prompt = generate_prompt("Jinja2 is a popular templating engine in the Python ecosystem.", 3)
print(prompt)

這個 example 輸出的長相如下:

Some text is provided below. Given the text, extract up to 3 keywords from the text. Avoid stopwords.
---------------------
Jinja2 is a popular templating engine in the Python ecosystem.
---------------------
Provide keywords in the following comma-separated format: 'KEYWORDS: <keywords>'

RAG 小註解

Rag 聽起來就像一塊破布. 但是在 AI 領域還滿紅的! 不同於普通的破布, 這個 RAG 是 Retrieval Augmented Generation 的縮寫. 看 keyword 就知道包括檢索 – 增強 – 生成. 整個功能的目標還是做生成式 (generative) AI.

那麼和普通的 LLM 差在哪裡呢? 普通的 LLM 學習了大量的知識, 但是可能有些專業領域沒學到, 或是還可以加強, 這時候就會用 RAG.

首先我們要把這些 “新知" 進行編碼, 在自然語言處理當中會用到 Embedding 技術, 把普通的文字轉換成向量. 此處我們既然想依賴既有的 LLM model, 當然我們要把我們新知和 LLM 的習知, mapping 到同一個空間去! 此時就用到了增強 ( augmented ) 這部分.

Step 1: 找到 Embedding 模型

from sentence_transformers import SentenceTransformer
encoder = SentenceTransformer('一個 EMBEDDING 模型')

Step 2: 為新知建立向量空間

這裡有個熱身的步驟, 先在 memory 當中產生一個 instance.

from qdrant_client import QdrantClient, models
qdrant = QdrantClient(":memory:")

接下來就可以設定新知的參數, 主要是 size 和 distance.

qdrant.recreate_collection(
    collection_name="新知的名稱",
    vectors_config=models.VectorParams(
        size=encoder.get_sentence_embedding_dimension(),
        distance=models.Distance.COSINE
    )
)

Step 3: 把新知的內容搬到向量空間.

其中 data 當然就是由 index (idx) 和 doc 組成.

qdrant.upload_records(
    collection_name="新知的名稱",
    records=[
        models.Record(
            id=idx,
            vector=encoder.encode(doc["新知的內容"]).tolist(),
            payload=doc
        ) for idx, doc in enumerate(data)
    ]
)

Step 4: 以文字在向量空間檢索 (retrieval) 得分最高的新知.

answer = qdrant.search(
    collection_name="新知",
    query_vector=encoder.encode("針對新知的問題").tolist(),
    limit=1 # 想要前幾高分的回答, 例如 1,3,5 
)

for ans in answer:
    print(ans.payload, "score:", ans.score)

由於這個新知的 database 知道的東西比較偏門, 它怎麼跟大語言模型共用呢? 答案就是把上述 RAG 的結果當作 LLM 的提示, 這樣 LLM 就會去 RAG 的輸出找答案.

Step 5: RAG 跟 LLM 互助合作

底下是叫 Copilot 寫的範例. 示意 RAG 的結果被 LLAMA2 拿去參考. 實用性不高, 但畢竟整合起來了.

search_results = [ans.payload for ans in answer] # 上面的新知

# Import necessary libraries
import os
import pinecone
from langchain.llms import Replicate
from langchain.vectorstores import Pinecone
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.chains import ConversationalRetrievalChain

# Set your API keys
os.environ['REPLICATE_API_TOKEN'] = "YOUR_REPLICATE_API_KEY"
pinecone.init(api_key='YOUR_PINECONE_API_KEY', environment='YOUR_PINECONE_ENVIRONMENT')

# Initialize Llama-2 components
replicate = Replicate()
pinecone_store = Pinecone()
text_splitter = CharacterTextSplitter()
embeddings = HuggingFaceEmbeddings()
retrieval_chain = ConversationalRetrievalChain()

# Example query from the user
user_query = "What are the health benefits of red wine?"

# Retrieve relevant information from search_results (assuming it contains relevant data)
relevant_data = search_results  # Replace with actual relevant data

# Process the user query
query_vector = embeddings.encode(text_splitter.split(user_query))

# Retrieve relevant responses using the retrieval chain
retrieved_responses = retrieval_chain.retrieve(query_vector, pinecone_store)

# Generate an answer based on the retrieved responses
answer = replicate.generate_answer(user_query, retrieved_responses, relevant_data)

print(f"Chatbot's response: {answer}")

用這個方法, 就不需要重 train 大語言模型, 也不影響 LLM 原本的實力. 但看官一定可以發現, 同一個問題必須分別或是依序丟給 RAG 和 LLM, 此時 RAG 才能產出東西給 LLM 當小抄 (in-context prompting). 這就是它的缺點.

使用 Vector Store 並非唯一的方式, 想要學習 WIKI, database, …. 都是可行的. 只要能把它變成 prompt 就可以改善 LLM 資訊不夠新 (knowledge cut off) 的幻覺 (Hallucination) 問題.

Give Myself a Pat on the Back

以前連假的時候, 我都會趁機讀完一兩本書. 不過這次我用它來完成我網課的最後一哩路. 這是一個 Coursera 的 AI TensorFlow Developer 學程, 裡面包括 4 個子課程.

Introduction to TensorFlow for Artificial Intelligence, Machine Learning, and Deep Learning
Convolutional Neural Networks in TensorFlow

Natural Language Processing in TensorFlow

Sequences, Time Series and Prediction

這個課程大概需要兩個月, 本來我想我好歹也算有個一知半解吧! 打算在免費試用七天的時間就把課程走完, 不過它還是有點難度, 所以我破功了. 只好為它花了註冊費. 接著我以生日前通過為目標, 失敗! 改為 2024/3/31 前通過, 又失敗! 總算在清明連假最後一天通過最後一個 Lab 了! 花了快一個月之久.

model = tf.keras.models.Sequential([ 
    tf.keras.layers.Conv1D(64, input_shape = [64, 1], kernel_size = 3, padding = 'causal', activation = "relu"),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32,return_sequences = True)),
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
    tf.keras.layers.Dense(32, activation = "relu"),
    tf.keras.layers.Dense(16, activation = "relu"),
    tf.keras.layers.Dense(1),
    tf.keras.layers.Lambda(lambda x: x* 25)
]) 
model.compile(loss= tf.keras.losses.Huber(),
optimizer=tf.keras.optimizers.SGD(momentum = 0.9, learning_rate = 5e-4),
metrics=["mae"])

最後一堂課的結語是 give yourself a pat on the back. 所以我就拿它來當標題. 接下來找下一個可以學的東西.

生成式 AI 大材小用記

原本聽說三星的 S24 應該有很厲害的 AI, 結果玩了一天看不出任何不同的地方?? 假如我沒有買到假貨的話, 應該就是我不會用!!

上網找了一篇比較新的評測, 終於知道有哪些功能可以用了. 但所有特異功能幾乎都是預設關閉. 難怪我覺得跟掛掉的 S20 用起來幾乎沒差別.

在即時翻譯的部分, 要進到設定 –> 進階功能 –> 進階智慧工具, 然後把電話、三星鍵盤、翻譯助理一一打開. 翻譯還要下載語言包, 小包的越南文、泰文需要 300 MB 上下, 大包的中英日韓接近 450MB. 這些都要個別下載. 測試起來雖然有時會認錯, 不過我覺得還堪用, 這樣就不用買科大訊飛翻譯機了.

至於那個圈圖搜尋, 首先 APP 要相容. 初步試過 Chrome, Disney+ 不行, Netflix 可以操作但會隱藏掉 video, 只留 OSD. 而 Facebook 和 YouTube 可以. 另外, 不是想畫圈就畫圈, 我試了一下要先按 Android 底部的那個圓圈圈 home icon, 接下來才能圈或是框要搜尋的對象. 初步測試效果沒有很好. 不過以圖找圖大概就是那種程度.

在照片的部分, 生成式照片要先在照片編輯時, 選編輯 (一隻筆的 icon), 再按花花的藍底三星 icon, 才會進入生成功能頁面.

用手寫筆歪七扭八地把水管框住, 它就會自己貼到物件的輪廓上.

被框住的物件可以移動或是消除. 我選消除. 水管就拿掉了. 當然會有一點痕跡在. 被蓋掉的英文字也還拚不出來, 但以後進版應該有機會.

另外一個樣本是今年的尾牙, 當我們吃到甜湯的時候, 桌上已經一堆雜物了. 畢竟沒發垃圾桶, 座位又擠, 總不能塞到隔壁同仁那邊. 所以拍起來是這樣.

靠著 AI 生成技術, 衛生紙拿走了. 灑出來的湯汁也清掉了, 質感好了很多! 唉, 我真是讓它完全大材小用啊.