Amazon 聊天機器人實作導航

我在 Amazon [1] 看到一個聊天機器人的課, 只需要花 1.5 小時. 我想說這時間我行啊, 就跟著跑了一遍流程.

不過呢? 跟某些網課一樣, 它只帶你走流程. 做完之後覺得有點空虛. 所以決定再複習一次我在哪裡? 我是誰? 我做了什麼?

Step 1: 選定 LLM. 在 SageMaker 下找到課程指定的 model (Flan T5 XL), 設置 Inference 用的端點 (endpoint). 也就是下圖中間的部分.

Step 2: 賦予 LLM 網路搜尋的能力. Amazon 的套件叫做 Kendra, 也就是上圖下方偏左的綠色方塊. Kendra 可以從指定的 data source 找資料進來, 例如 shared point, one drive. 這樣就滿有實用性. 但課程建議選 Amazon S3.

Step 3: 賦予語音文字轉換的能力. 採用的套件是 Amazon Lex, 也就是上圖下方偏右的綠色方塊. 預設它的 Inten (意圖?) 會叫 Kendra 去 search. 等等! 那 LLM 在幹啥? 原來會把問題和 Kendra 打包去問 LLM. 官方課程說明如下:

  • Retrieve relevant information from your Kendra index.
  • Package that information along with the original question into a prompt.
  • Send the prompt to your LLM endpoint.
  • Return the LLM’s response to your Lex V2 bot.

到這一步, 網頁上已經可以有 build 和 test 的按鈕可以選. Build 完可以 test, 但回答一般問題的能力很差.

Step 4: 增加 RAG 能力. 在上圖上方的藍色區塊是 Amazon Cloud9 這個 IDE 環境. 我們在這裡編出 RAG 的 code. 它綁定:

  • Your AWS REGION
  • Your Account ID
  • Your Kendra index ID
  • Your Large Language Model endpoint name

然後把它們 build 出一個 docker image, 放到 repository. 不意外地, 它必須是 Amazon 家的 ECR (Elastic Contain Registry).

這一動需要一些時間, 可以休息一下.

這邊的架構採用常見的 RAG + LangChain [3]. 也就是上面架構圖下方的橘色 Lambda 字樣那個方塊. RAG 和 LangChain 是少數沒看見 Amazon logo 的地方.

Step 5: 做好的 Docker 放在哪裡跑呢? 當然要順便推銷一下 Amazon Lambda [2]. 它號稱不用佈署後台就可以執行, 而且有免費方案.

所以我們在 Lambda create function 並指定 image 的 URI 就是 Amazon ECR image repository 底下的 rag-kendra-llm-lex 這個 docker. 然後設定 IAM (Intent, Access Management) 的參數.

Step 6. 搞定後台之後, 接著要搞定前台. 首先在 Amazon 的 Lex, 做一些規定好 (=死) 的設定. 建立出一個名叫 WebAppBot 的機器人. 為了讓 Amazon 前台後台能夠相認, 不意外前台也有一個 ID, 例如: V5R1NOLHRM.

Step 7. 現在前台後台都有了. 還缺什麼呢? 缺一個雲端平台讓人找到 WebAppBot! 這時候 Amazon Cloudformation [4] 就出來了.

等等! 我剛剛以為 Lambda 不就搞定所有了? 可惜並不是. Lambda 處理的是雲端運算的資源. Cloudformation 處理的是 “Speed up cloud provisioning with infrastructure as cod". 也就是寫寫 code 就設定好雲端了. 兩者的功用的不同恰如章節名稱所示.

TASK 4.2: DEPLOY THE IMAGE AS A LAMBDA FUNCTION

Task 5: Deploy a web app with Cloudformation

Cloudformation 的資源叫做 stack, 所以我們 create stack, 取名叫 LexWebApp, 並且指定連到前述 WebAppBot (ID), 和 AccountID.

光是網頁設定還不夠. 此處要真正 build LexWebApp, 例如指定跑在 AMD 還是 X86 上等底層的 code. 這時用到 Amazon CodeBuild.

Step 8. 感覺應該都沒事了. 但課程說 webAPP 不應該連到 LexBot, 而是連到它的 alias. 這樣才能一面背景維護, 又維持服務在線. 所以重 build 連到 alias 的 lambda 了. 在 cloudformation 以 WebAppUrl 指到的 URL launch LexWebApp.

OK! 這就是我對這個課程的理解. 它用了置入性行銷的手法, 把自家的產品介紹了一輪. 我有學到新的東西嗎? 有, 我對 Amazon 賣的東西更理解了. 但是在技術上, 有點空虛就是了. 所以我又用了一天來回顧我花的 1.5 小時, 讓它更有意義一點!

[REF]

  1. https://explore.skillbuilder.aws/
  2. https://aws.amazon.com/tw/pm/lambda/
  3. RAG實作教學,LangChain + Llama2 |創造你的個人LLM
  4. https://aws.amazon.com/tw/cloudformation/

程式語言雜記

最近幾天在天竺國出差, 剛好有機會面試印度人. 我問到: 既然您的專長是 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) 問題.