程式語言雜記

最近幾天在天竺國出差, 剛好有機會面試印度人. 我問到: 既然您的專長是 Android, 為何語言的專長是 C 和 C++, 沒有 Java 呢? 印度人說, 因為他做 Android 時已經升管理職, 所以沒有 coding Java 的機會.

話說一隻手指指向別人, 就有四隻指頭指向自己. 當初 Sun Micron 找工研院電通所合作發展 Java 下線時, 我也去受訓拿到一張 Java 講師證. 但後來沒機會用上, 日久愈來愈生疏, 最後也覺得還是藏拙比較不尷尬. 哈!

至於 C++, 我剛好有個網路課程只剩幾個小時沒上完. 正好就今天處理了. 畢竟從天竺轉機回台灣這兩天, 加起來睡不到十小時, 累到幾乎無法思考大事. 這麼難用的時間, 碰上簡單的課程和超簡單的最後一個作業, 真是天作之合. (Adjacency List 那個作業就難多了, 題意說明落落長, class 定義在哪裡要自己找出來).

這門課雖然用到一些 C++, 重點還是講資料結構. 例如: Dijkstra’s algorithm wasn’t able to find the shortest path if edge has negative weight. 翻譯成白話是: 假如我們的工作流程中有人扯後腿, 怎麼優化都會鬼打牆. 基本上這堂課還不錯.

當初會上 Coursera 是為了學 AI. 為了發揮最大投資效益, 我買了一年Plus 會員吃到飽來學習 Tensorflow, LLM, 和其他 AI 的訓練課程. 基本上能選的課, 我聽得差不多了, 甚至還產生了心得. 像是同樣的生成式 AI 課程, Google 版重視 AI 倫理, Amazon 版重視 AWS 生態系實作, IBM 版重視如何用在 project 管理, DeepLearning AI 重視知識完整性等等.

其中上過最硬的課算是 Scrimba 的 Learn Embeddings and Vector Databases (RAG 相關). 因為我 Java script 確實有點生鏽了, 跟不太上講師 trace code. 基本上, 各門各派用的語言都不同, 加強程式語言能力才能了解實作細節, 體會複雜度, 甚至是交程式作業 (現在電腦改程式作業好簡單). 這些能力不是 AI 可以替我們感同身受的, 也是最不會被 AI 取代的 – 至少我這麼認為.

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. 所以我就拿它來當標題. 接下來找下一個可以學的東西.