DEV Community

Jambo
Jambo

Posted on

來 Azure 學習 OpenAI 四 - 用 Embedding 加強 GPT

大家好,我是學生大使 Jambo。在我們前一篇文章中,我們介紹了 OpenAI 模型的調用。今天,我將為大家介紹 Embedding 的使用。

嵌入是什麼

嵌入(Embedding )是一種將高維數據映射到低維空間的方法。嵌入可以將高維數據可視化,也可以用於聚類、分類等任務。嵌入可以是線性的,也可以是非線性的。在深度學習中,我們通常使用非線性嵌入。非線性嵌入通常使用神經網絡實現。

上面這句話對於沒接觸過 NLP(自然語言處理)的同學來說可能有點抽象。你可以理解為通過嵌入,可以將文字信息壓縮、編碼成向量(或者不准確的稱之為數組),而這個向量包含了這段文字的語義。我們可以將這個技術用於搜索引擎、推薦系統等等。

調用 Embedding 模型

與前篇一樣,我們需要先部署模型。這裡我們使用 text-embedding-ada-002

1

然後安裝 openai 包。用以下命令安裝,會將 numpy、pandas 等庫一併安裝。

pip install openai[datalib]
Enter fullscreen mode Exit fullscreen mode

接下來導入 openai,並做一些初始化工作。

import openai

openai.api_key = "REPLACE_WITH_YOUR_API_KEY_HERE"    # Azure 的密鑰
openai.api_base = "REPLACE_WITH_YOUR_ENDPOINT_HERE"  # Azure 的終結點
openai.api_type = "azure" 
openai.api_version = "2023-03-15-preview" # API 版本,未來可能會變
model = "text-embedding-ada-002"  # 模型的部署名
Enter fullscreen mode Exit fullscreen mode
embedding = openai.Embedding.create(
    input="蘋果", engine="text-embedding-ada-002"
)
print(embedding1)
Enter fullscreen mode Exit fullscreen mode
{
  "data": [
    {
      "embedding": [
        0.011903401464223862,
        -0.023080304265022278,
        -0.0015027695335447788,
        ...
    ],
      "index": 0,
      "object": "embedding"
    }
  ],
  "model": "ada",
  "object": "list",
  "usage": {
    "prompt_tokens": 3,
    "total_tokens": 3
  }
}
Enter fullscreen mode Exit fullscreen mode

其中 embedding 就是 “蘋果” 所對應的向量。

計算向量相似度

在我們將文字轉換成向量之後,我們討論兩句話的相似度,其實就是問它們所對應向量的相似度。通常我們使用餘弦相似度來衡量兩個向量的相似度。

餘弦相似度是計算兩個向量夾角角度的 $\cos$ 值,取值範圍在 -1 和 1 之間。如果兩個向量的方向完全一致,那麼它們的餘弦相似度為 1;如果兩個向量的方向完全相反,那麼它們的餘弦相似度為 -1;如果兩向量是垂直(正交)的,那麼它們的餘弦相似度為 0。其公式如下:

$$
\cos(\theta) = \frac{\vec A \cdot \vec B}{|\vec A| |\vec B|}
$$

$\vec A$ 和 $\vec B$ 分別是兩個向量,$\theta$ 是兩個向量的夾角。而 $|\vec A|$ 和 $|\vec B|$ 分別是向量 $\vec A$ 和 $\vec B$ 的長度(模長)。因為 OpenAI 的 Embedding 模型返回的是單位向量,即向量的長度為 1,所以我們不需要計算模長,而它們的夾角就是兩個向量的點積。

$$
\cos(\theta) = \frac{\vec A \cdot \vec B}{1 \cdot 1} = \vec A \cdot \vec B
$$

有的人可能會疑惑為什麼不用歐式距離來計算。在這種向量長度都為 1 的情況下,歐式距離和余弦相似度其實是等價的,它們之間是可以互相轉換的。

在 Python 中,我們可以使用 numpy 庫來計算兩個數列的餘弦相似度:

import numpy as np

# 計算兩個向量的餘弦相似度
def cosine_similarity(a, b):
    return np.dot(a, b)  # 計算點積
Enter fullscreen mode Exit fullscreen mode

下面是個簡單的例子:

embedding1 = openai.Embedding.create(
    input="蘋果", engine="text-embedding-ada-002"
)["data"][0]["embedding"]
embedding2 = openai.Embedding.create(
    input="apple", engine="text-embedding-ada-002"
)["data"][0]["embedding"]
embedding3 = openai.Embedding.create(
    input="鞋子", engine="text-embedding-ada-002"
)["data"][0]["embedding"]

print(cosine_similarity(embedding1, embedding2))
print(cosine_similarity(embedding1, embedding3))
print(cosine_similarity(embedding2, embedding3))
Enter fullscreen mode Exit fullscreen mode
0.8823086919469535
0.8256366789720779
0.7738048005367909
Enter fullscreen mode Exit fullscreen mode

用 Embedding 加強 GPT 的能力

GPT模型非常強大,它能夠根據訓練的數據回答問題。但是,它只能回答那些在訓練時獲取到的知識,對於訓練時獲取不到的知識,它一無所知。所以對於類似“明天天氣如何”,或者企業內部的一些專業知識,GPT模型就無能為力了。

天氣等及時性較強的內容,我們可以通過搜索引擎獲得相關的信息。而像新聞報導或是文檔這類內容,通常篇幅較長,可 GPT 模型能處理的文字有限,因此我們需要將其分割成多個段落,然後找到其中最相關的段落,再將其與問題一起傳入 GPT 模型中。而如何找到最相關的段落呢?這就需要用到 Embedding 模型了。將分割後的段落傳入 Embedding 模型,得到每個段落的向量,然後計算與問題的相似度,最後找到最相似的段落。特別是本地文檔,我們可以事先將其轉換成向量,然後保存下來,這樣就可以快速地找到最相關的段落。

下面我用 Codon 的文檔作為資料來源,並讓 GPT 可以根據文檔裡的內容進行回答。

預處理文檔

我用下面的代碼將文檔分割成多個段落,並且保證每段字數不超過 2048:

import os
import pandas

MAX_LEN = 2048


def split_text(text, max_length=2048):
    paragraphs = text.split("\n")
    result = []
    current_paragraph = ""
    for paragraph in paragraphs:
        if len(current_paragraph) + len(paragraph) > max_length:
            result.append(current_paragraph)
            current_paragraph = paragraph
        else:
            current_paragraph += "\n" + paragraph
    if current_paragraph:
        result.append(current_paragraph)
    return result


def find_md_files(directory):
    result = []
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.endswith(".md"):
                result.append(os.path.join(root, file))
    return result


if __name__ == "__main__":
    df = pandas.DataFrame(columns=["file", "content"])
    for file in find_md_files("."):
        with open(file) as f:
            text = f.read()
        for c in split_text(text, MAX_LEN):
            df.loc[len(df)] = [file, c]

    df.to_csv("output.csv", index=False)

Enter fullscreen mode Exit fullscreen mode

然後將這些段落傳入 Embedding 模型,得到每個段落的向量。這裡我沒有使用異步,這是為了避免觸發 API 的速率限制。為了演示方便,我只是將數據保存在 csv 文件中,實際使用時,我們可以將數據保存到 Pinecone,Milvus 等向量數據庫中。

import openai
import pandas

openai.api_key = ""
openai.api_base = ""
openai.api_type = "azure"
openai.api_version = "2023-03-15-preview"
model = "text-embedding-ada-002"


def get_embedding(text):
    response = openai.Embedding.create(input=text, engine="text-embedding-ada-002")
    embedding = response["data"][0]["embedding"]
    assert len(embedding) == 1536
    return embedding


def main():
    df = pandas.read_csv("output.csv")
    embeddings = [get_embedding(text) for text in df["content"]]
    df["embedding"] = embeddings
    df.to_csv("docs.csv", index=False)


if __name__ == "__main__":
    import time

    star = time.time()
    main()
    print(f"Time taken: {time.time() - star}")

Enter fullscreen mode Exit fullscreen mode

製作 Prompt

為了讓 GPT 只回答文檔裡的內容,我們首先要將這件事告訴 GPT,並且我們還需要傳入與問題相關的段落。

prompt_prefix = """
你是一個客服,回答用戶關於文檔的問題。
僅使用以下資料提供的事實進行回答。如果下面沒有足夠的信息,就說你不知道。不要生成不使用以下資料的答案。

資料:
{sources}
"""
Enter fullscreen mode Exit fullscreen mode

有時我們提問的問題可能會與先前的對話相關,因此為了更好的匹配文檔段落,我們將對話歷史和新的問題告訴 GPT,並讓它幫我們生成一個查詢語句。

summary_prompt_template = """
以上是到目前為止的對話記錄,下面我將提出一個新問題,需要通過在知識庫中搜索相關的條目來回答問題。根據以上的對話記錄和下面的新問題,生成一個英文的查詢語句,用於在知識庫中搜索相關的條目。你只需要回答查詢的語句,不用加其他任何內容。

新問題:
{question}
"""
Enter fullscreen mode Exit fullscreen mode

生成查詢語句

我們首先先定義一些幫助函數:

def cos_sim(a, b):
    return np.dot(a, b)


def get_chat_answer(messages: dict, max_token=1024):
    return openai.ChatCompletion.create(
        engine=chat_model,
        messages=messages,
        temperature=0.7,
        max_tokens=max_token,
    )["choices"][0]["message"]


def get_embedding(text):
    return openai.Embedding.create(
        engine=embed_model,
        input=text,
    )["data"][
        0
    ]["embedding"]


docs = pd.read_csv("docs.csv", converters={"embedding": eval})
pd.set_option("display.max_colwidth", None) # 顯示完整的文本
history = []

Enter fullscreen mode Exit fullscreen mode

history 是用來儲存對話歷史。在下面的代碼中如果 history 為空,那麼我們就直接使用用戶的輸入作為查詢語句,否則我們就使用 GPT 生成的查詢語句。要注意的是,我是把歷史記錄和生成查詢的請求拼在一起輸入給模型的,沒有把請求放到 history 中。

user_input = ""

if len(history) == 0:
    query = user_input
else:
    query = get_chat_answer(
        history
        + [
            {
                "role": "user",
                "content": summary_prompt_template.format(question=user_input),
            }
        ],
        max_token=32,
    )["content"]

print(f"Searching: {query}")
Enter fullscreen mode Exit fullscreen mode

搜索最相關的段落

我用 pandas 將先前保存好的段落和對應向量讀取出來,然後計算查詢語句和每個段落的相似度,最後拿到最相似的段落。當然,如果相似度不夠高,我們就告訴 GPT “no information”。

docs = pd.read_csv("data.csv", converters={"embedding": eval})

query_embedding = get_embedding(query)
dot_products = np.dot(np.stack(docs["embedding"].values), query_embedding)
top_index = np.argsort(dot_products)[-1:]
top_content = (
    docs.iloc[top_index]["content"].to_string(index=False)
    if dot_products[top_index] > 0.8
    else "no information"
)
Enter fullscreen mode Exit fullscreen mode

生成回答

現在我們獲取到了相關的信息,接下來我們將相關的信息和問題一起傳入 GPT,讓它生成回答。這裡因為我用的是 GPT-3,他對 system 的內容沒有那麼看重,所以我用了 user 的身份來傳入最開始我們設定的 prompt,並手動編寫了一個回答來強化 GPT 對於我們的提示的理解。這句話和上面生成查詢語句的請求一樣,並沒有放到 history 中。但我們有將 GPT 的回答放進去。

history.append({"role": "user", "content": user_input})

massage = [
    {"role": "user", "content": prompt_prefix.format(sources=top_content)},
    {
        "role": "assistant",
        "content": "好的,我只會根據以上提供的資料提供的內容回答問題,我不會回答不使用資源的內容。",
    },
] + history
res = get_chat_answer(massage)
print(res["content"])
history.append(res)
print("-" * 50, end="\n\n")
Enter fullscreen mode Exit fullscreen mode

接下來我們可以來嘗試一下,我先輸入一個問題:“Python Codon 是什麼?”

[Searching: Python Codon 是什麼? ]
Codon 是一個高性能的 Python 編譯器,它可以將 Python 代碼編譯成本地機器代碼,而不需要任何運行時開銷。 Codon 的性能通常與 C/C++ 相當,而且還支持本地多線程,可以實現更高的加速比。此外,Codon 還可以通過插件基礎架構進行擴展,可以輕鬆地集成新的庫、編譯器優化和關鍵字等。
--------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

2

作為對比,我們來看看 ChatGPT 的回答:

3

可見在 ChatGPT 的訓練集中,並沒有 Codon 相關的信息,因此他無法給出我們想要的答案。而我們通過 Embedding 的方式,找到 Codon 相關的資料,然後將其傳入 GPT,讓 GPT 生成答案,這樣就可以得到我們想要的答案了。

當然,在實際的應用中,代碼絕對不會這麼簡單,我們還需要考慮很多問題,比如如何儲存和更新知識庫,如何處理對話的輸入和輸出,如何管理對話的歷史等等。但是,這些都不是我們今天要討論的問題,我們今天只是想要討論一下 Embedding 與 GPT 的結合,以及如何將文字轉換為 Embedding。

而 Embedding 的用法也不只是這一種。得益於向量的可測距性,我們還可以將其用於聚類、分類、推薦,甚至是可視化等等,這些都是我們今天沒有討論的內容。

Top comments (1)

Collapse
 
chungyih profile image
chungyi

如果用openai sdk裡的cosine_similarity function跟單純用np.dot運算後得到的結果會不一樣, 比較推薦用哪種方式呢?