Attentio機制以及應用

最近生成語言模型相當的流行,因此想到它的開源始祖transformer架構。

Google Brain及Research兩個團隊於2017年所提出的論文「Attention Is All You Need」,是首次應用注意力機制(Attention Mechanism)於深度學習模型,其效果和效率讓大眾驚豔,因此發展持續至今形成了現今生成式AI的浪潮。其中最令人意想不到的,最早開發出Transformer的Google並未得到好處,反而讓OpenAI、微軟、Nvidia…等公司大起,隨著這波浪潮再次迎來高峰。

注意力與自我注意力

注意力機制(Attention Mechanism)這個聽起來很具人性思維的名詞,其實最早應用於電腦視覺,但後來在NLP(自然語言處理)領域發揚光大並被廣泛使用。它的核心思想是「將有限的注意力集中在重要訊息上,進而達到節省資源並快速獲得有效的訊息」。

另一種也經常聽到的「自我注意力機制(Self-Attention Mechanism)」,其實是Attention的一種變體,它們都能捕捉輸入序列中的依賴關係,不過兩者的工作方式有如下差異:

注意力機制:在處理序列資料時,模型的焦點只集中於與當前預測最相關的那一部分,亦即,當模型生成輸出序列的某個元素時,注意力機制會讓模型特別注意輸入序列中與當前輸出最相關的那一部分。

自我注意力機制:除了注意輸入序列中的每個元素與當前預測的關聯之外,Self-Attention還考慮到輸入序列中各個元素彼此之間的關聯。這代表當模型在生成輸出序列的元素時,Self-Attention可讓模型注意到所有輸入序列元素與當前輸出相關的部分,並考慮這些部分之間的「互動」。

那什麼是序列元素之間的互動呢?

它指的是:語義上的、語法上的或者是上下文中的關聯性。例如:

我把我的書給了他。

在這個句子中,「我」、「書」和「他」之間存在著關聯性。「我」是動作的主體,「書」是被給予的對象,「他」是接受動作的對象。這些單詞之間的關聯性就是所謂的:互動。而Self-Attention比起Attention更能夠捕捉到這些關聯性並用於生成輸出。

Transformer的架構

基於注意力機制而開發的encoder-decoder模型,我們通稱為Transformer模型,這類的模型完全不使用遞歸或卷積網路。其架構請參考下方說明及圖片。(下方文字為補充圖中未說明的部份)

Encoder和Decoder都各有幾組由「Add & Norm」+「Multi-Head Attention」合併的網路層,而「Add & Norm」+「Masked Multi-Head Attention」僅出現在Decoder。

  • Add & Norm層:可讓模型在訓練過程中保持穩定並訓練更深的網路。Transformer模型的每個子層之後都會使用此Add & Norm組件。

Add🡪 針對殘差連接(Residual Connection),將殘差的輸入直接加到其輸出上,避免梯度消失問題。

Norm🡪 進行層正規化(Layer Normalization),對輸出進行正規化,使其均值為0方差為1。

一張含有 文字, 字型, 螢幕擷取畫面, 行 的圖片

自動產生的描述

當我們在訓練模型時,Decoder的輸入是目標序列(也就是答案Y),如果沒有把這個答案序列往右移一位,那模型就會看到該元素本身或者之後的元素(也就是偷看到答案)。因此訓練時會在答案序列的開頭加入一個起始符號(例如),並移除序列的最後一個字詞。

用程式示範模擬注意力機制

下方的程式示範了基本的attention方法,可看出神奇的attention其實本質上是一種加權方法,它透過存放在一個與輸入資料相同維度的attention energy table(範例程式中該table內容為隨機產生),乘上所輸入的序列(也就是輸入的內容)後,較重要的值就會比較大因其加權較高(也就是感興趣的部份)。

注意這個attention energy table是可以學習的,模型會持續在訓練過程中修正,以學習到最正確的attention。

# 引入 PyTorch 和其神經網路模組
import torch
import torch.nn as nn

# 定義注意力機制類別,繼承自 nn.Module
class Attention(nn.Module):
    def __init__(self, hidden_size):
        super(Attention, self).__init__()  # 調用父類別的初始化方法
        self.hidden_size = hidden_size  # 設定隱藏層的大小
        self.attn = nn.Linear(hidden_size, hidden_size)  # 定義一個線性轉換,輸入和輸出的維度都是 hidden_size
        self.v = nn.Parameter(torch.rand(hidden_size))  # 定義一個可學習的權重向量

    def forward(self, hidden, encoder_outputs):
        seq_len = encoder_outputs.size(0)  # 獲取輸入序列的長度
        attn_energies = torch.zeros(seq_len)  # 初始化注意力能量向量

        # 對每個輸入的編碼器輸出,計算其與隱藏狀態的注意力能量
        for i in range(seq_len):
            attn_energies[i] = self.score(hidden, encoder_outputs[i])

        # 將注意力能量向量進行 softmax 轉換,得到注意力權重
        return torch.softmax(attn_energies, dim=0)

    def score(self, hidden, encoder_output):
        energy = self.attn(encoder_output)  # 對編碼器輸出進行線性轉換
        energy = energy.view(-1)  # 將轉換後的結果轉換為一維向量
        v = self.v.view(-1)  # 將權重向量轉換為一維向量
        return torch.dot(v, energy)  # 計算權重向量和能量向量的點積,得到注意力能量

# 範例使用
hidden_size = 256  # 設定隱藏層的大小
attention = Attention(hidden_size)  # 創建注意力機制模型

# 創建隨機的隱藏狀態和編碼器輸出
hidden = torch.rand(1, hidden_size)
encoder_outputs = torch.rand(10, hidden_size)

# 計算注意力權重
attention_weights = attention(hidden, encoder_outputs)

print("Attention weights:", attention_weights)  # 輸出注意力權重

輸出:

Attention weights: tensor([3.1191e-03, 2.3164e-04, 2.9727e-02, 8.8525e-03, 2.4755e-03, 3.9005e-02, 1.8735e-03, 5.5221e-01, 3.5848e-01, 4.0233e-03], grad_fn=)

Transformer的各項任務

Transformer目前己大量運用在生成式AI中,若細分其應用類別,大致可分類如以下的任務:

  • 文字分類
  • 閱讀理解
  • 克漏字
  • 文字生成
  • 命名實體辨識
  • 文字摘要
  • 翻譯

HuggingFace提供了一個巨大的模型庫,其中包含了一些非常成熟好用的模型,在一般運用上我們可以直接使用不需要再retraining或finetune便能得到不錯的預測結果,我們經常聽到「Zero Shot Learning」,就是指這類的模型。

要如何使用這些模型呢?HuggingFace提供了一種稱為Pipeline(管道)的工具,我們只要提供給它需要的任務類型,管理工具會自動選擇合適的模型來執行並提供預測結果。這個工具相當簡單好用,最大的目的是提供作為立即的測試,以評估模型是否需要再訓練。

下以使用了數種任務來示範HuggingFace的pipeline的操作。可發現我們需要對不同的任務,來選擇不同的模型或使用不同的參數。(不過目前發展中的Prompt Tuning,改變了這種指定任務的作法並且逐漸取代fine-tuning的趨勢,這部份後續再來研究)。

  • .文字分類 sentiment-analysis
from transformers import pipeline

classifier = pipeline("sentiment-analysis")
result = classifier("I hate you")[0]
print(result)
result = classifier("I love you")[0]
print(result)
result = classifier("我不喜歡看電影")[0]
print(result)
result = classifier("他很愛去唱歌")[0]
print(result)

執行結果:

{‘label’: ‘NEGATIVE’, ‘score’: 0.9991129040718079}

{‘label’: ‘POSITIVE’, ‘score’: 0.9998656511306763}

{‘label’: ‘NEGATIVE’, ‘score’: 0.9617491364479065}

{‘label’: ‘POSITIVE’, ‘score’: 0.7244700789451599}

上面的pipeline並沒有指定用什麼模型,因此huggingface會自動選擇最適合sentiment-analysis任務的「英文版本」模型。因此,有時候我們必須手動指定模型,如果跑出的結果不滿意的話。

  • 閱讀理解 question-answering

使用Pipeline來處理閱讀理解任務也相當方便,不過如果我們輸入的是中文的內容,那麼就需要多幾個步驟,也就是指定中文模型及tokenizer,否則HuggingFace預設使用英文模型會得到不適合的答案。

from transformers import pipeline

model_name = "bert-base-chinese"

q_answer = pipeline(task="question-answering", model=model_name, tokenizer=model_name)

context = r"""
彰化縣埔鹽鄉1間畜牧場在尚未取得畜牧場登記前,即偷跑飼養逾3萬隻雞,
近來該場大量雞毛飄散到場外,汙染排水、農田、果園及民宅,當地居民及農民氣得不斷檢舉。
彰化縣政府環保局與農業處表示,今天已派員稽查,若查有不法,將開罰。
業者說,對周遭鄰居很抱歉,已著手改善中。
"""
result = q_answer(question="水、農田、果園及民宅的污染原因是什麼?", context=context)
print(result)

result = q_answer(question="那一個單位負責管理稽核環境問題?", context=context)
print(result)

執行結果:

{‘score’: 0.000236633830354549, ‘start’: 129, ‘end’: 133, ‘answer’: ‘改善中。’}

{‘score’: 0.0002376682823523879, ‘start’: 129, ‘end’: 133, ‘answer’: ‘改善中。’}

看起來使用bert-base-chinese效果並不好,我們改成其它的模型,程式修改如下:

from transformers import pipeline
from transformers import BertTokenizerFast, BertForQuestionAnswering

model_name = "NchuNLP/Chinese-Question-Answering"
model = BertForQuestionAnswering.from_pretrained(model_name)
tokenizer = BertTokenizerFast.from_pretrained(model_name)

q_answer = pipeline(task="question-answering", model=model, tokenizer=tokenizer)

context = r"""
彰化縣埔鹽鄉1間畜牧場在尚未取得畜牧場登記前,即偷跑飼養逾3萬隻雞,
近來該場大量雞毛飄散到場外,汙染排水、農田、果園及民宅,當地居民及農民氣得不斷檢舉。
彰化縣政府環保局與農業處表示,今天已派員稽查,若查有不法,將開罰。
業者說,對周遭鄰居很抱歉,已著手改善中。
"""

result = q_answer(question="水、農田、果園及民宅的污染原因是什麼?", context=context)
print(result)

result = q_answer(question="那一個單位負責管理稽核環境問題?", context=context)
print(result)

執行結果:

{‘score’: 0.9981415867805481, ‘start’: 40, ‘end’: 54, ‘answer’: ‘大量雞毛飄散到場外,汙染排水’}

{‘score’: 1.4056818809128494e-15, ‘start’: 79, ‘end’: 87, ‘answer’: ‘彰化縣政府環保局’}

果然這次的答案就比較正確了。

  • 克漏字 fill-mask
from transformers import pipeline

model_name = "bert-base-chinese"
unmasker = pipeline(task="fill-mask", model=model_name, tokenizer=model_name)

sentence = "每次回鄉,只宅在老家的我們,這次決定過家"+unmasker.tokenizer.mask_token+"而不入。"

result = unmasker(sentence)
print(result)

執行結果:

[

{‘score’: 0.9773150682449341, ‘token’: 7271, ‘token_str’: ‘‘, ‘sequence’: ‘每 次 回 鄉 , 只 宅 在 老 家 的 我 們 , 這 次 決 定 過 家 而 不 入 。’},

{‘score’: 0.01137497927993536, ‘token’: 1139, ‘token_str’: ‘‘, ‘sequence’: ‘每 次 回 鄉 , 只 宅 在 老 家 的 我 們 , 這 次 決 定 過 家 而 不 入 。’},

{‘score’: 0.000959079887252301, ‘token’: 1920, ‘token_str’: ‘‘, ‘sequence’: ‘每 次 回 鄉 , 只 宅 在 老 家 的 我 們 , 這 次 決 定 過 家 而 不 入 。’},

{‘score’: 0.0006299608503468335, ‘token’: 2431, ‘token_str’: ‘‘, ‘sequence’: ‘每 次 回 鄉 , 只 宅 在 老 家 的 我 們 , 這 次 決 定 過 家 而 不 入 。’},

{‘score’: 0.0004984855186194181, ‘token’: 6624, ‘token_str’: ‘‘, ‘sequence’: ‘每 次 回 鄉 , 只 宅 在 老 家 的 我 們 , 這 次 決 定 過 家 而 不 入 。’}

]

  • 文字生成 text-generation
from transformers import pipeline
from transformers import BertTokenizer, GPT2LMHeadModel, TextGenerationPipeline

model_name = "uer/gpt2-chinese-poem"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = GPT2LMHeadModel.from_pretrained(model_name)

txt_gen = pipeline(task="text-generation", model=model_name, tokenizer=model_name)
sentence = "君自故鄉來,應知故鄉事。來日綺窗前,寒梅著花未?"

result = txt_gen(sentence, max_length=90, do_sample=False, truncation=True)
print(result)

執行結果:

[{‘generated_text’: ‘君自故鄉來,應知故鄉事。來日綺窗前,寒梅著花未? , 一 一 媵 媵 媵 。 以 一 枝 花 , 一 一 媵 媵 媵 诗 魂 。 一 媵 诗 媵 诗 魂 , 一 一 媵 诗 魂 。 此 一 盅 酒 , 一 酹 一 杯 酹 。 此 一 盅 酒 。 此 一 盅 酒 一 盅 , 一 酹 一 酹’}]

生成的文字並不算好,但看在gpt2-chinese-poem是一個小小不到1G的模型就算了,您可以把model替換為其它大型的模型試看看。

  • 命名實體辨識 ner
from transformers import pipeline
from transformers import BertTokenizer, AutoModelForTokenClassification

model_name = "ckiplab/bert-base-chinese-ner"

tokenizer = BertTokenizer.from_pretrained(model_name)
model = AutoModelForTokenClassification.from_pretrained(model_name)

ner = pipeline(task="ner", model=model, tokenizer=tokenizer)

sentence = "在今年的GTC大會上,英偉達的黃仁勳以一種充滿儀式感的方式邀請了Transformer的七位作者(NikiParmar因故臨時未能出席)參與圓桌論壇的討論,這是七位作[>

for r in ner(sentence):
    print(r)

執行結果:

{‘entity’: ‘B-DATE’, ‘score’: 0.9999993, ‘index’: 2, ‘word’: ‘今’, ‘start’: None, ‘end’: None}

{‘entity’: ‘E-DATE’, ‘score’: 0.9999988, ‘index’: 3, ‘word’: ‘年’, ‘start’: None, ‘end’: None}

{‘entity’: ‘B-PERSON’, ‘score’: 0.9999995, ‘index’: 14, ‘word’: ‘黃’, ‘start’: None, ‘end’: None}

{‘entity’: ‘I-PERSON’, ‘score’: 0.9999989, ‘index’: 15, ‘word’: ‘仁’, ‘start’: None, ‘end’: None}

{‘entity’: ‘E-PERSON’, ‘score’: 0.999998, ‘index’: 16, ‘word’: ‘勳’, ‘start’: None, ‘end’: None}

{‘entity’: ‘S-CARDINAL’, ‘score’: 0.99999785, ‘index’: 33, ‘word’: ‘七’, ‘start’: None, ‘end’: None}

{‘entity’: ‘S-CARDINAL’, ‘score’: 0.99999774, ‘index’: 60, ‘word’: ‘七’, ‘start’: None, ‘end’: None}