自然語言處理中的文字生成任務複雜且運算成本高,選擇合適的解碼策略至關重要。本文將探討各種解碼策略,包含貪婪搜尋、束搜尋和多種取樣方法,並提供程式碼範例說明如何在 Python 中使用 Transformers 函式庫實作這些策略。同時,文章也解釋了為何使用對數機率計算序列機率,以及如何透過調整引數控制生成文字的多樣性和連貫性。

import pandas as pd
import torch
import numpy as np
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModelForCausalLM

# 初始化模型和分詞器
model_name = "gpt2"  # 可替換成其他模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

# 計算對數機率的函式
def log_probs_from_logits(logits, labels):
    logp = F.log_softmax(logits, dim=-1)
    logp_label = torch.gather(logp, 2, labels.unsqueeze(2)).squeeze(-1)
    return logp_label

def sequence_logprob(model, labels, input_len=0):
    with torch.no_grad():
        output = model(labels)
        log_probs = log_probs_from_logits(output.logits[:, :-1, :], labels[:, 1:])
        seq_log_prob = torch.sum(log_probs[:, input_len:])
    return seq_log_prob.cpu().numpy()

# 輸入文字
input_txt = """In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.\n\n"""
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
max_length = 128

# 貪婪搜尋
output_greedy = model.generate(input_ids, max_length=max_length, do_sample=False)
print("貪婪搜尋:", tokenizer.decode(output_greedy[0]))

# 束搜尋
output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, no_repeat_ngram_size=2, early_stopping=True)
print("束搜尋:", tokenizer.decode(output_beam[0]))

# 溫度取樣
output_temp = model.generate(input_ids, max_length=max_length, do_sample=True, temperature=0.7)
print("溫度取樣:", tokenizer.decode(output_temp[0]))

# Top-k 取樣
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True, top_k=50)
print("Top-k 取樣:", tokenizer.decode(output_topk[0]))

# Nucleus 取樣
output_topp = model.generate(input_ids, max_length=max_length, do_sample=True, top_p=0.90)
print("Nucleus 取樣:", tokenizer.decode(output_topp[0]))

內容解密:

  1. 初始化: 載入預訓練的 GPT-2 模型和分詞器,並設定運算裝置。
  2. log_probs_from_logits 函式: 計算給定 logits 和標籤的對數機率。
  3. sequence_logprob 函式: 計算整個序列的對數機率。
  4. 輸入文字: 設定輸入的提示文字,並使用分詞器轉換為模型可接受的輸入格式。
  5. 貪婪搜尋: 使用 model.generate 函式並設定 do_sample=False 進行貪婪搜尋。
  6. 束搜尋: 使用 model.generate 函式,並設定 num_beamsno_repeat_ngram_sizeearly_stopping 引數進行束搜尋。
  7. 溫度取樣: 使用 model.generate 函式,並設定 do_sample=Truetemperature 引數進行溫度取樣。
  8. Top-k 取樣: 使用 model.generate 函式,並設定 do_sample=Truetop_k 引數進行 Top-k 取樣。
  9. Nucleus 取樣: 使用 model.generate 函式,並設定 do_sample=Truetop_p 引數進行 Nucleus 取樣。
  10. 輸出: 將生成的文字使用分詞器解碼並輸出。

不同解碼策略適用於不同場景。貪婪搜尋速度快,但容易產生重複且缺乏多樣性。束搜尋能提升文字品質,但計算成本較高。取樣方法能增加多樣性,但需要調整溫度、top-k 或 top-p 等引數以平衡多樣性和連貫性。選擇哪種策略取決於任務需求和可用的計算資源。

貪婪搜尋解碼(Greedy Search Decoding)與束搜尋解碼(Beam Search Decoding)

在自然語言處理的文字生成任務中,解碼策略扮演著至關重要的角色。本篇將探討兩種常見的解碼方法:貪婪搜尋解碼和束搜尋解碼,並分析其優缺點。

貪婪搜尋解碼

貪婪搜尋解碼是一種簡單直接的解碼策略。在每個時間步,它選擇具有最高機率的下一個詞彙,並將其附加到輸入序列中,重複此過程直到達到預定的最大長度或遇到結束符號(EOS)。

程式碼實作

import pandas as pd
import torch

# 設定初始輸入文字和引數
input_txt = "Transformers are the"
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
iterations = []
n_steps = 8
choices_per_step = 5

# 進行貪婪搜尋解碼
with torch.no_grad():
    for _ in range(n_steps):
        iteration = dict()
        iteration["Input"] = tokenizer.decode(input_ids[0])
        output = model(input_ids=input_ids)
        
        # 取得最後一個詞彙的logits並套用softmax函式
        next_token_logits = output.logits[0, -1, :]
        next_token_probs = torch.softmax(next_token_logits, dim=-1)
        sorted_ids = torch.argsort(next_token_probs, dim=-1, descending=True)
        
        # 儲存最可能的五個詞彙
        for choice_idx in range(choices_per_step):
            token_id = sorted_ids[choice_idx]
            token_prob = next_token_probs[token_id].cpu().numpy()
            token_choice = f"{tokenizer.decode(token_id)} ({100 * token_prob:.2f}%)"
            iteration[f"Choice {choice_idx+1}"] = token_choice
        
        # 將預測的下一個詞彙附加到輸入序列
        input_ids = torch.cat([input_ids, sorted_ids[None, 0, None]], dim=-1)
        iterations.append(iteration)

# 顯示結果
pd.DataFrame(iterations)

內容解密:

  1. 初始設定:定義輸入文字、迭代次數和每步選擇的候選詞彙數量。
  2. 模型輸出:使用模型預測下一個詞彙的機率分佈。
  3. 排序與選擇:根據機率對候選詞彙進行排序,並選擇最可能的詞彙。
  4. 迭代更新:將選定的詞彙附加到輸入序列,並重複此過程。

使用generate()函式重現貪婪搜尋

input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
output = model.generate(input_ids, max_new_tokens=n_steps, do_sample=False)
print(tokenizer.decode(output[0]))

內容解密:

  1. generate()函式:使用Transformers函式庫中的generate()函式來生成文字。
  2. max_new_tokens:指定生成的最大詞彙數量。
  3. do_sample=False:關閉取樣,啟用貪婪搜尋。

生成獨角獸故事

max_length = 128
input_txt = """In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.\n\n"""
input_ids = tokenizer(input_txt, return_tensors="pt")["input_ids"].to(device)
output_greedy = model.generate(input_ids, max_length=max_length, do_sample=False)
print(tokenizer.decode(output_greedy[0]))

內容解密:

  1. 輸入提示:定義一個包含獨角獸發現故事的輸入提示。
  2. 生成文字:使用貪婪搜尋生成更長的故事。

貪婪搜尋的缺點

貪婪搜尋容易產生重複的輸出序列,這在新聞文章等需要多樣性的文字生成任務中是不理想的。

束搜尋解碼

束搜尋解碼透過跟蹤前b個最可能的下一個詞彙(稱為束或部分假設)來改進貪婪搜尋。它透過考慮現有集合的所有可能的下一個詞彙擴充套件並選擇b個最可能的擴充套件來選擇下一組束。重複此過程直到達到最大長度或EOS。

內容解密:

  1. 多樣性提升:束搜尋透過考慮多個候選序列來提高生成文字的多樣性。
  2. 計算複雜度:相較於貪婪搜尋,束搜尋需要更多的計算資源。

總之,貪婪搜尋和束搜尋各有其優缺點。貪婪搜尋簡單快速,但可能導致重複的輸出。束搜尋透過考慮多個候選序列來提高多樣性,但計算成本更高。在實際應用中,應根據具體需求選擇合適的解碼策略。

Beam Search 解碼技術詳解

在自然語言處理領域,Beam Search是一種廣泛使用的解碼演算法,尤其是在文字生成任務中。它透過在每個時間步保留多個可能的輸出序列,最終選擇最有可能的序列作為最終輸出。

為什麼使用對數機率而非機率本身?

計算序列的整體機率涉及到計算條件機率的乘積。由於每個條件機率通常是[0,1]之間的小數,將它們相乘可能導致整體機率下溢(underflow),即計算結果小於電腦可以精確表示的最小值。例如,假設有一個長度為1024的序列,並且每個詞元的機率為0.5,那麼整體機率將是一個極小的數字:

0.5 ** 1024
# 輸出:5.562684646268003e-309

這種情況會導致數值不穩定。為了避免這個問題,我們可以使用對數機率。對數機率可以將機率的乘積轉換為對數機率的加總,從而避免下溢的問題。

對數機率計算範例

import numpy as np
sum([np.log(0.5)] * 1024)
# 輸出:-709.7827128933695

程式碼解密:

  1. 0.5 ** 1024:計算0.5的1024次方,展示了直接電腦率可能導致的下溢問題。
  2. sum([np.log(0.5)] * 1024):透過對數運算將乘積轉換為加總,避免了下溢問題。
    • np.log(0.5) 計算0.5的對數。
    • sum([...] * 1024) 對1024個相同的對數值進行求和。

Beam Search 的實作與對比

為了評估Beam Search的效果,我們可以比較它與貪婪解碼(Greedy Decoding)在生成文字上的對數機率。首先,我們需要定義一個函式來計算給定序列的對數機率。

計算對數機率的函式實作

import torch.nn.functional as F

def log_probs_from_logits(logits, labels):
    logp = F.log_softmax(logits, dim=-1)
    logp_label = torch.gather(logp, 2, labels.unsqueeze(2)).squeeze(-1)
    return logp_label

def sequence_logprob(model, labels, input_len=0):
    with torch.no_grad():
        output = model(labels)
        log_probs = log_probs_from_logits(output.logits[:, :-1, :], labels[:, 1:])
        seq_log_prob = torch.sum(log_probs[:, input_len:])
    return seq_log_prob.cpu().numpy()

程式碼解密:

  1. F.log_softmax(logits, dim=-1):對模型的logits輸出進行log softmax操作,得到對數機率分佈。
    • dim=-1 表示在最後一個維度上進行softmax操作。
  2. torch.gather(logp, 2, labels.unsqueeze(2)).squeeze(-1):從對數機率分佈中收集真實標籤對應的對數機率。
    • labels.unsqueeze(2) 將標籤張量增加一個維度,以便與logp對齊。
    • squeeze(-1) 移除最後一個維度,得到最終的對數機率。
  3. sequence_logprob函式:計算給定序列的總對數機率。
    • output.logits[:, :-1, :]labels[:, 1:] 的對齊確保了我們計算的是每個詞元在給定前文條件下的對數機率。

Beam Search 與貪婪解碼的比較

透過比較貪婪解碼和Beam Search生成的文字及其對數機率,我們可以觀察到Beam Search通常能夠獲得更高的對數機率,即更好的生成品質。

output_greedy = model.generate(input_ids, max_length=max_length)
logp_greedy = sequence_logprob(model, output_greedy, input_len=len(input_ids[0]))

output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, do_sample=False)
logp_beam = sequence_logprob(model, output_beam, input_len=len(input_ids[0]))

結果對比

  • 貪婪解碼生成的文字及其對數機率:-87.43
  • Beam Search生成的文字及其對數機率:-55.23

程式碼解密:

  1. model.generate:使用模型生成文字。
    • num_beams=5 指定了Beam Search的寬度。
    • do_sample=False 表示不進行隨機取樣,而是選擇最可能的詞元。
  2. sequence_logprob:計算生成文字的對數機率,用於比較不同解碼策略的效果。

如何解決重複文字問題?

Beam Search可能會產生重複的文字。為瞭解決這個問題,可以使用no_repeat_ngram_size引數來施加n-gram懲罰,避免生成已經出現過的n-gram。

output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, do_sample=False, no_repeat_ngram_size=3)

程式碼解密:

  • no_repeat_ngram_size=3:禁止生成重複的三元組(3-gram),從而減少文字的重複性。

文字生成中的取樣方法探討

在前面的章節中,我們探討瞭如何使用束搜尋(Beam Search)來改善文字生成的品質。本章節將繼續深入文字生成的技術,重點介紹取樣方法(Sampling Methods)在提升生成文字多樣性方面的應用。

取樣方法的基本原理

最簡單的取樣方法是從模型輸出的機率分佈中隨機選取詞彙。模型的輸出機率分佈可以表示為:

$$ P(y_t = w_i | y_{<t}, \mathbf{A}) = \text{softmax}(z_{t,i}) = \frac{\exp(z_{t,i})}{\sum_{j=1}^{V} \exp(z_{t,j})} $$

其中 $V$ 表示詞彙表的大小。

溫度引數的引入

為了控制輸出的多樣性,我們可以引入溫度引數 $T$,它能夠重新調整 logits 的值,然後再進行 softmax 運算:

$$ P(y_t = w_i | y_{<t}, \mathbf{A}) = \frac{\exp(z_{t,i}/T)}{\sum_{j=1}^{V} \exp(z_{t,j}/T)} $$

當 $T \ll 1$ 時,機率分佈趨向於集中在機率最高的詞彙上,減少罕見詞彙的出現。相反,當 $T \gg 1$ 時,機率分佈趨向於均勻分佈,每個詞彙的機率趨於相等。

實驗結果與分析

output_temp = model.generate(input_ids, max_length=max_length, do_sample=True, temperature=2.0, top_k=0)
print(tokenizer.decode(output_temp[0]))

輸出結果

在令人震驚的發現中,科學家在安第斯山脈一個偏遠、以前未被探索過的山谷中發現了一群獨角獸。令研究人員更驚訝的是,這些獨角獸竟然說著流利的英語。 雖然……(後續內容因溫度引數設定較高,出現了無意義的文字)

output_temp = model.generate(input_ids, max_length=max_length, do_sample=True, temperature=0.5, top_k=0)
print(tokenizer.decode(output_temp[0]))

輸出結果

在令人震驚的發現中,科學家在安第斯山脈一個偏遠、以前未被探索過的山谷中發現了一群獨角獸。令研究人員更驚訝的是,這些獨角獸竟然說著流利的英語。 科學家們正在尋找神秘聲音的來源,這聲音讓動物們笑和哭。 這些獨角獸生活在安第斯山脈的一個偏遠山谷中。 「當我們第一次聽到動物的聲音時,我們以為是一頭獅子或老虎,」布宜諾斯艾利斯大學的研究人員路易斯·古茲曼說。

內容解密:

上述程式碼展示瞭如何透過調整溫度引數來控制文字生成的多樣性與連貫性。當溫度引數較高(如 $T = 2.0$)時,生成的文字多樣性增加,但連貫性下降,出現了無意義的文字。相反,當溫度引數較低(如 $T = 0.5$)時,生成的文字更具連貫性,但多樣性降低。

Top-k 與 Nucleus 取樣

除了調整溫度引數外,還可以透過截斷詞彙機率分佈來控制生成文字的多樣性。Top-k 和 Nucleus(Top-p)取樣是兩種常見的方法,它們透過限制每個時間步可取樣的詞彙數量來實作。

Top-k 取樣

Top-k 取樣在每個時間步只從機率最高的 $k$ 個詞彙中進行取樣。

Nucleus(Top-p)取樣

Nucleus 取樣則是根據累積機率達到某個閾值 $p$ 來動態決定需要考慮的詞彙數量。

# 使用 Top-k 取樣
output_topk = model.generate(input_ids, max_length=max_length, do_sample=True, top_k=50)
print(tokenizer.decode(output_topk[0]))

內容解密:

此段程式碼演示瞭如何使用 Top-k 取樣來限制每個時間步的可取樣詞彙數量。透過設定 top_k=50,模型在每個時間步只會從機率最高的 50 個詞彙中進行取樣,從而在保持一定多樣性的同時,避免生成過於罕見或無意義的詞彙。

文字生成中的解碼策略

在前面的章節中,我們探討了自然語言理解(NLU)任務,而文字生成則是另一種完全不同的任務。文字生成需要至少一次前向傳遞(forward pass)來生成每個標記(token),如果使用束搜尋(beam search),則需要更多次的前向傳遞。這使得文字生成在計算上非常耗時,因此需要合適的基礎設施來執行大規模的文字生成模型。此外,一個好的解碼策略可以將模型的輸出機率轉換為離散的標記,從而提高文字品質。

解碼策略的重要性

選擇正確的解碼策略對於文字生成的品質至關重要。不同的解碼策略會對生成的文字產生不同的影響。在本章中,我們將探討幾種常見的解碼策略,包括貪婪搜尋(greedy search)、束搜尋(beam search)、抽樣(sampling)以及它們的變體,如top-k抽樣和nucleus抽樣(top-p抽樣)。

貪婪搜尋與束搜尋

貪婪搜尋是一種簡單的解碼策略,它在每個步驟中選擇機率最高的標記。然而,這種方法可能會導致生成的文字品質不佳,因為它忽略了其他可能的標記。

output_greedy = model.generate(input_ids, max_length=max_length, do_sample=False)
print(tokenizer.decode(output_greedy[0]))

束搜尋則透過在每個步驟中保留多個候選序列來改進貪婪搜尋。這種方法可以提高生成的文字品質,但也會增加計算成本。

output_beam = model.generate(input_ids, max_length=max_length, num_beams=5, no_repeat_ngram_size=2, early_stopping=True)
print(tokenizer.decode(output_beam[0]))

內容解密:

  • num_beams=5 表示在束搜尋中保留5個候選序列。
  • no_repeat_ngram_size=2 防止生成的文字中出現重複的二元語法(bigram)。
  • early_stopping=True 表示當找到足夠好的候選序列時,提前停止搜尋。

抽樣方法

抽樣是一種根據機率的解碼策略,它根據模型的輸出機率分佈來選擇標記。這種方法可以增加生成的文字的多樣性。

output_sample = model.generate(input_ids, max_length=max_length, do_sample=True)
print(tokenizer.decode(output_sample[0]))

內容解密:

  • do_sample=True 表示啟用抽樣。

然而,簡單的抽樣可能會導致生成的文字中出現低機率的標記,從而影響文字品質。為瞭解決這個問題,人們提出了top-k抽樣和nucleus抽樣。

Top-k抽樣與Nucleus抽樣

Top-k抽樣透過限制抽樣的範圍到機率最高的k個標記來避免低機率的選擇。

output_topk = model.generate(input_ids, max_length=max_length, do_sample=True, top_k=50)
print(tokenizer.decode(output_topk[0]))

內容解密:

  • top_k=50 表示只從機率最高的50個標記中進行抽樣。

Nucleus抽樣則透過設定一個機率品質閾值(如95%)來動態地確定抽樣的範圍。

output_topp = model.generate(input_ids, max_length=max_length, do_sample=True, top_p=0.90)
print(tokenizer.decode(output_topp[0]))

內容解密:

  • top_p=0.90 表示從累積機率達到90%的標記中進行抽樣。

哪種解碼策略最好?

不幸的是,沒有一種普遍最佳的解碼策略。最佳的解碼策略取決於具體的任務和需求。如果需要模型執行精確的任務,如算術或回答特定問題,那麼應該降低溫度或使用確定性方法,如貪婪搜尋結合束搜尋。如果希望模型生成更長的文字並具有一定的創造性,那麼應該切換到抽樣方法並提高溫度,或使用top-k和nucleus抽樣的混合方法。

最終檢查

  • 徹底清除內部標記且零容忍任何殘留
  • 強制驗證結構完整性及邏輯性
  • 強制確認技術深度及台灣本土化語言風格
  • 強制驗證程式碼邏輯完整性及「#### 內容解密」逐項詳細作用與邏輯之解說
  • 強制確認內容完全原創且充分重構
  • 強制確認圖表標題不包含「Plantuml」字眼
  • 強制確認每段程式碼後都有「#### 內容解密:」詳細每個段落作用與邏輯之解說