LLM 課程文件

摘要

Hugging Face's logo
加入 Hugging Face 社群

並獲得增強的文件體驗

開始使用

摘要

Ask a Question Open In Colab Open In Studio Lab

在本節中,我們將探討如何使用 Transformer 模型將長文件濃縮成摘要,這項任務稱為_文字摘要_。這是最具挑戰性的 NLP 任務之一,因為它需要一系列能力,例如理解長段落和生成能夠捕捉文件主要主題的連貫文字。然而,如果做得好,文字摘要是一個強大的工具,可以透過減輕領域專家詳細閱讀長文件的負擔來加快各種業務流程。

儘管 Hugging Face Hub 上已經存在各種針對摘要任務進行微調的模型,但幾乎所有這些模型都只適用於英文文件。因此,為了在本節中增加一些趣味性,我們將訓練一個英西雙語模型。在本節結束時,你將擁有一個可以總結客戶評論的模型,如下所示:

正如我們將看到的,這些摘要之所以簡潔,是因為它們是從客戶在產品評論中提供的標題中學到的。讓我們首先為這項任務準備一個合適的雙語語料庫。

準備多語言語料庫

我們將使用多語言亞馬遜評論語料庫來建立我們的雙語摘要器。這個語料庫包含六種語言的亞馬遜產品評論,通常用於基準測試多語言分類器。然而,由於每條評論都附帶一個簡短的標題,我們可以使用這些標題作為我們模型學習的目標摘要!為了開始,讓我們從 Hugging Face Hub 下載英語和西班牙語子集。

from datasets import load_dataset

spanish_dataset = load_dataset("amazon_reviews_multi", "es")
english_dataset = load_dataset("amazon_reviews_multi", "en")
english_dataset
DatasetDict({
    train: Dataset({
        features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
        num_rows: 200000
    })
    validation: Dataset({
        features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
        num_rows: 5000
    })
    test: Dataset({
        features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'],
        num_rows: 5000
    })
})

如你所見,每種語言的 `train` 分割都有 200,000 條評論,`validation` 和 `test` 分割各有 5,000 條評論。我們感興趣的評論資訊包含在 `review_body` 和 `review_title` 列中。讓我們透過建立一個簡單的函式來檢視一些示例,該函式使用我們在第 5 章中學到的技術從訓練集中隨機抽取樣本。

def show_samples(dataset, num_samples=3, seed=42):
    sample = dataset["train"].shuffle(seed=seed).select(range(num_samples))
    for example in sample:
        print(f"\n'>> Title: {example['review_title']}'")
        print(f"'>> Review: {example['review_body']}'")


show_samples(english_dataset)
'>> Title: Worked in front position, not rear'
'>> Review: 3 stars because these are not rear brakes as stated in the item description. At least the mount adapter only worked on the front fork of the bike that I got it for.'

'>> Title: meh'
'>> Review: Does it’s job and it’s gorgeous but mine is falling apart, I had to basically put it together again with hot glue'

'>> Title: Can\'t beat these for the money'
'>> Review: Bought this for handling miscellaneous aircraft parts and hanger "stuff" that I needed to organize; it really fit the bill. The unit arrived quickly, was well packaged and arrived intact (always a good sign). There are five wall mounts-- three on the top and two on the bottom. I wanted to mount it on the wall, so all I had to do was to remove the top two layers of plastic drawers, as well as the bottom corner drawers, place it when I wanted and mark it; I then used some of the new plastic screw in wall anchors (the 50 pound variety) and it easily mounted to the wall. Some have remarked that they wanted dividers for the drawers, and that they made those. Good idea. My application was that I needed something that I can see the contents at about eye level, so I wanted the fuller-sized drawers. I also like that these are the new plastic that doesn\'t get brittle and split like my older plastic drawers did. I like the all-plastic construction. It\'s heavy duty enough to hold metal parts, but being made of plastic it\'s not as heavy as a metal frame, so you can easily mount it to the wall and still load it up with heavy stuff, or light stuff. No problem there. For the money, you can\'t beat it. Best one of these I\'ve bought to date-- and I\'ve been using some version of these for over forty years.'

✏️ 試試看! 更改 `Dataset.shuffle()` 命令中的隨機種子,以探索語料庫中的其他評論。如果你是西班牙語使用者,請檢視 `spanish_dataset` 中的一些評論,看看標題是否也像是合理的摘要。

這個樣本展示了通常在網上發現的評論的多樣性,從積極到消極(以及介於兩者之間的所有內容!)。儘管帶有“meh”標題的示例資訊量不大,但其他標題看起來像是評論本身的不錯摘要。在所有 40 萬條評論上訓練摘要模型在單個 GPU 上需要太長時間,所以我們將重點放在為單個產品領域生成摘要。為了瞭解我們可以選擇哪些領域,讓我們將 `english_dataset` 轉換為 `pandas.DataFrame` 並計算每個產品類別的評論數量。

english_dataset.set_format("pandas")
english_df = english_dataset["train"][:]
# Show counts for top 20 products
english_df["product_category"].value_counts()[:20]
home                      17679
apparel                   15951
wireless                  15717
other                     13418
beauty                    12091
drugstore                 11730
kitchen                   10382
toy                        8745
sports                     8277
automotive                 7506
lawn_and_garden            7327
home_improvement           7136
pet_products               7082
digital_ebook_purchase     6749
pc                         6401
electronics                6186
office_product             5521
shoes                      5197
grocery                    4730
book                       3756
Name: product_category, dtype: int64

英語資料集中最受歡迎的產品是家居用品、服裝和無線電子產品。然而,為了保持亞馬遜的主題,我們將專注於總結書評——畢竟,這是公司賴以建立的基礎!我們可以看到兩個符合條件的產商品類別(`book` 和 `digital_ebook_purchase`),所以讓我們只過濾這兩種語言資料集中的這些產品。正如我們在第 5 章中看到的,`Dataset.filter()` 函式允許我們非常高效地切分資料集,因此我們可以定義一個簡單的函式來完成此操作。

def filter_books(example):
    return (
        example["product_category"] == "book"
        or example["product_category"] == "digital_ebook_purchase"
    )

現在,當我們對 `english_dataset` 和 `spanish_dataset` 應用此函式時,結果將只包含涉及圖書類別的那些行。在應用過濾器之前,讓我們將 `english_dataset` 的格式從 `"pandas"` 切換回 `"arrow"`。

english_dataset.reset_format()

然後我們可以應用過濾函式,並作為健全性檢查,讓我們檢查一些評論樣本,看看它們是否確實是關於書籍的。

spanish_books = spanish_dataset.filter(filter_books)
english_books = english_dataset.filter(filter_books)
show_samples(english_books)
'>> Title: I\'m dissapointed.'
'>> Review: I guess I had higher expectations for this book from the reviews. I really thought I\'d at least like it. The plot idea was great. I loved Ash but, it just didnt go anywhere. Most of the book was about their radio show and talking to callers. I wanted the author to dig deeper so we could really get to know the characters. All we know about Grace is that she is attractive looking, Latino and is kind of a brat. I\'m dissapointed.'

'>> Title: Good art, good price, poor design'
'>> Review: I had gotten the DC Vintage calendar the past two years, but it was on backorder forever this year and I saw they had shrunk the dimensions for no good reason. This one has good art choices but the design has the fold going through the picture, so it\'s less aesthetically pleasing, especially if you want to keep a picture to hang. For the price, a good calendar'

'>> Title: Helpful'
'>> Review: Nearly all the tips useful and. I consider myself an intermediate to advanced user of OneNote. I would highly recommend.'

好的,我們可以看到評論不完全是關於書籍的,也可能涉及日曆和電子應用程式(如 OneNote)。儘管如此,這個領域似乎很適合訓練摘要模型。在我們研究適用於此任務的各種模型之前,我們還有最後一項資料準備工作要做:將英語和西班牙語評論合併為一個 `DatasetDict` 物件。🤗 Datasets 提供了一個便捷的 `concatenate_datasets()` 函式,顧名思義,它將兩個 `Dataset` 物件堆疊在一起。因此,為了建立我們的雙語資料集,我們將遍歷每個拆分,連線該拆分的資料集,並打亂結果以確保我們的模型不會過度擬合單一語言。

from datasets import concatenate_datasets, DatasetDict

books_dataset = DatasetDict()

for split in english_books.keys():
    books_dataset[split] = concatenate_datasets(
        [english_books[split], spanish_books[split]]
    )
    books_dataset[split] = books_dataset[split].shuffle(seed=42)

# Peek at a few examples
show_samples(books_dataset)
'>> Title: Easy to follow!!!!'
'>> Review: I loved The dash diet weight loss Solution. Never hungry. I would recommend this diet. Also the menus are well rounded. Try it. Has lots of the information need thanks.'

'>> Title: PARCIALMENTE DAÑADO'
'>> Review: Me llegó el día que tocaba, junto a otros libros que pedí, pero la caja llegó en mal estado lo cual dañó las esquinas de los libros porque venían sin protección (forro).'

'>> Title: no lo he podido descargar'
'>> Review: igual que el anterior'

這看起來確實是英語和西班牙語評論的混合!現在我們有了訓練語料庫,最後一件要檢查的事情是評論及其標題中的詞語分佈。這對於摘要任務尤其重要,因為資料中較短的參考摘要可能會使模型偏向於在生成的摘要中只輸出一兩個詞。下面的圖表顯示了詞語分佈,我們可以看到標題嚴重偏向於只有 1-2 個詞。

Word count distributions for the review titles and texts.

為了解決這個問題,我們將過濾掉標題過短的示例,以便我們的模型可以生成更有趣的摘要。由於我們處理的是英語和西班牙語文字,我們可以使用一個粗略的啟發式方法來根據空格分割標題,然後使用我們值得信賴的 `Dataset.filter()` 方法,如下所示:

books_dataset = books_dataset.filter(lambda x: len(x["review_title"].split()) > 2)

現在我們已經準備好語料庫,接下來看看我們可以對它進行微調的幾種 Transformer 模型!

文字摘要模型

如果你仔細想想,文字摘要與機器翻譯是相似的任務:我們有一段文字(比如評論),我們希望將其“翻譯”成一個較短的版本,捕捉輸入的顯著特徵。因此,大多數用於摘要的 Transformer 模型都採用我們在第 1 章中首次遇到的編碼器-解碼器架構,儘管也有一些例外,例如 GPT 模型系列,它們也可以用於少量樣本設定下的摘要任務。下表列出了一些流行的預訓練模型,可以對它們進行微調以用於摘要。

Transformer 模型 描述 多語言?
GPT-2 儘管 GPT-2 被訓練為自迴歸語言模型,但你可以透過在輸入文字末尾附加“TL;DR”來使其生成摘要。
PEGASUS 使用預訓練目標來預測多句文字中的掩碼句子。這個預訓練目標比普通語言建模更接近摘要,並且在流行的基準測試中得分很高。
T5 一種通用的 Transformer 架構,將所有任務都 формулирует 為文字到文字框架;例如,模型用於摘要文件的輸入格式是 `summarize: ARTICLE`。
mT5 T5 的多語言版本,在多語言 Common Crawl 語料庫 (mC4) 上預訓練,涵蓋 101 種語言。
BART 一種新穎的 Transformer 架構,具有編碼器和解碼器堆疊,經過訓練以重建損壞的輸入,結合了 BERT 和 GPT-2 的預訓練方案。
mBART-50 BART 的多語言版本,在 50 種語言上預訓練。

正如你從這張表中看到的,大多數用於摘要(以及大多數 NLP 任務)的 Transformer 模型都是單語言的。這對於像英語或德語這樣的“高資源”語言來說很好,但對於世界上使用的數千種其他語言來說就不那麼理想了。幸運的是,有一類多語言 Transformer 模型,如 mT5 和 mBART,可以解決這個問題。這些模型使用語言建模進行預訓練,但有一個特點:它們不是在一個語言語料庫上進行訓練,而是同時在 50 多種語言的文字上進行聯合訓練!

我們將重點關注 mT5,這是一種基於 T5 的有趣架構,它在文字到文字框架中進行了預訓練。在 T5 中,每個 NLP 任務都透過一個提示字首(如 `summarize:`)來 формулируется,該字首會使模型根據提示調整生成的文字。如下圖所示,這使得 T5 具有極強的通用性,因為您可以使用一個模型解決許多工!

Different tasks performed by the T5 architecture.

mT5 不使用字首,但與 T5 具有許多通用性,並且具有多語言優勢。現在我們已經選擇了模型,接下來看看如何準備資料以進行訓練。

✏️ 試試看! 完成本節後,透過使用相同的技術對 mT5 和 mBART 進行微調,比較它們的效果。作為加分項,你還可以嘗試僅對英語評論進行 T5 微調。由於 T5 有一個特殊的字首提示,你需要在下面的預處理步驟中將 `summarize:` 新增到輸入示例的前面。

資料預處理

我們的下一個任務是對評論及其標題進行標記化和編碼。像往常一樣,我們首先載入與預訓練模型檢查點關聯的標記器。我們將使用 `mt5-small` 作為檢查點,以便我們可以在合理的時間內微調模型。

from transformers import AutoTokenizer

model_checkpoint = "google/mt5-small"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

💡 在 NLP 專案的早期階段,一個好的做法是,在少量資料樣本上訓練一類“小型”模型。這使得你能夠更快地除錯和迭代,以實現端到端的工作流程。一旦你對結果有信心,你總是可以透過簡單地更改模型檢查點來擴大模型規模!

讓我們在一個小例子上測試 mT5 分詞器。

inputs = tokenizer("I loved reading the Hunger Games!")
inputs
{'input_ids': [336, 259, 28387, 11807, 287, 62893, 295, 12507, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]}

這裡我們可以看到在第 3 章的第一次微調實驗中遇到的熟悉的 `input_ids` 和 `attention_mask`。讓我們使用分詞器的 `convert_ids_to_tokens()` 函式解碼這些輸入 ID,看看我們正在處理哪種分詞器。

tokenizer.convert_ids_to_tokens(inputs.input_ids)
['▁I', '▁', 'loved', '▁reading', '▁the', '▁Hung', 'er', '▁Games', '</s>']

特殊的 Unicode 字元 ` ` 和序列結束符 ` ` 表示我們正在處理 SentencePiece 分詞器,它基於第 6 章中討論的 Unigram 分段演算法。Unigram 對於多語言語料庫特別有用,因為它允許 SentencePiece 不受重音、標點符號以及許多語言(如日語)沒有空格字元的事實的影響。

為了標記化我們的語料庫,我們必須處理與摘要相關的細微差別:由於我們的標籤也是文字,它們有可能超過模型的最大上下文大小。這意味著我們需要對評論及其標題應用截斷,以確保我們不會向模型傳遞過長的輸入。🤗 Transformers 中的標記器提供了一個巧妙的 `text_target` 引數,允許您與輸入並行地標記化標籤。以下是 mT5 如何處理輸入和目標的示例:

max_input_length = 512
max_target_length = 30


def preprocess_function(examples):
    model_inputs = tokenizer(
        examples["review_body"],
        max_length=max_input_length,
        truncation=True,
    )
    labels = tokenizer(
        examples["review_title"], max_length=max_target_length, truncation=True
    )
    model_inputs["labels"] = labels["input_ids"]
    return model_inputs

讓我們來看看這段程式碼,瞭解發生了什麼。我們做的第一件事是定義 `max_input_length` 和 `max_target_length` 的值,它們設定了評論和標題長度的上限。由於評論主體通常比標題長得多,我們相應地調整了這些值。

有了 `preprocess_function()`,使用我們在本課程中廣泛使用的便捷 `Dataset.map()` 函式來標記化整個語料庫就變得很簡單了。

tokenized_datasets = books_dataset.map(preprocess_function, batched=True)

現在語料庫已經預處理完畢,讓我們看看一些常用於摘要的指標。正如我們將看到的,在衡量機器生成文字的質量時,沒有萬能的解決方案。

💡 你可能已經注意到,我們在上面的 `Dataset.map()` 函式中使用了 `batched=True`。這會以 1,000(預設值)的批次編碼示例,並允許你利用 🤗 Transformers 中快速標記器的多執行緒功能。在可能的情況下,嘗試使用 `batched=True` 以充分利用你的預處理!

文字摘要的指標

與本課程中涵蓋的大多數其他任務相比,衡量摘要或翻譯等文字生成任務的效能並不那麼簡單。例如,對於像“我喜歡讀《飢餓遊戲》”這樣的評論,有多種有效的摘要,例如“我喜歡《飢餓遊戲》”或“《飢餓遊戲》是一部很棒的作品”。顯然,在生成的摘要和標籤之間應用某種精確匹配不是一個好的解決方案——即使是人類在這種指標下也會表現不佳,因為我們都有自己的寫作風格。

對於摘要,最常用的指標之一是 ROUGE 分數(Recall-Oriented Understudy for Gisting Evaluation 的縮寫)。這個指標的基本思想是將生成的摘要與一組通常由人類建立的參考摘要進行比較。為了更精確,假設我們要比較以下兩個摘要:

generated_summary = "I absolutely loved reading the Hunger Games"
reference_summary = "I loved reading the Hunger Games"

比較它們的一種方法是計算重疊詞的數量,在本例中為 6。但這有點粗糙,因此 ROUGE 基於計算重疊的_精確度_和_召回率_分數。

🙋 如果這是你第一次聽說精確度和召回率,請不用擔心——我們將一起透過一些明確的例子來使其清晰。這些指標通常在分類任務中遇到,因此如果你想了解精確度和召回率在該上下文中是如何定義的,我們建議查閱 `scikit-learn` 的指南

對於 ROUGE,召回率衡量生成的摘要捕獲了參考摘要多少內容。如果我們只是比較詞語,召回率可以根據以下公式計算:Recall=NumberofoverlappingwordsTotalnumberofwordsinreferencesummary \mathrm{Recall} = \frac{\mathrm{Number\,of\,overlapping\, words}}{\mathrm{Total\, number\, of\, words\, in\, reference\, summary}}

對於我們上面的簡單例子,這個公式給出了 6/6 = 1 的完美召回率;也就是說,參考摘要中的所有詞語都已被模型生成。這聽起來可能很棒,但想象一下,如果我們生成的摘要是“我真的非常喜歡通宵閱讀《飢餓遊戲》”。這也會有完美的召回率,但可以說這是一個更糟糕的摘要,因為它過於冗長。為了處理這些情況,我們還計算了精確度,在 ROUGE 的上下文中,它衡量了生成的摘要有多少是相關的:Precision=NumberofoverlappingwordsTotalnumberofwordsingeneratedsummary \mathrm{Precision} = \frac{\mathrm{Number\,of\,overlapping\, words}}{\mathrm{Total\, number\, of\, words\, in\, generated\, summary}}

將其應用於我們冗長的摘要,精確度為 6/10 = 0.6,這比我們較短的摘要獲得的 6/7 = 0.86 的精確度要差得多。在實踐中,通常會計算精確度和召回率,然後報告 F1 分數(精確度和召回率的調和平均值)。我們可以在 🤗 Datasets 中輕鬆完成此操作,首先安裝 `rouge_score` 包:

!pip install rouge_score

然後按照以下方式載入 ROUGE 指標:

import evaluate

rouge_score = evaluate.load("rouge")

然後我們可以使用 `rouge_score.compute()` 函式一次性計算所有指標。

scores = rouge_score.compute(
    predictions=[generated_summary], references=[reference_summary]
)
scores
{'rouge1': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)),
 'rouge2': AggregateScore(low=Score(precision=0.67, recall=0.8, fmeasure=0.73), mid=Score(precision=0.67, recall=0.8, fmeasure=0.73), high=Score(precision=0.67, recall=0.8, fmeasure=0.73)),
 'rougeL': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)),
 'rougeLsum': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92))}

哇,輸出中有大量資訊——這一切意味著什麼?首先,🤗 Datasets 實際上計算了精確度、召回率和 F1 分數的置信區間;這些就是你在這裡看到的 `low`、`mid` 和 `high` 屬性。此外,🤗 Datasets 計算了各種 ROUGE 分數,這些分數基於不同型別的文字粒度來比較生成的摘要和參考摘要。`rouge1` 變體是 unigram 的重疊——這只是一個花哨的說法,表示詞語的重疊,正是我們上面討論的指標。為了驗證這一點,讓我們提取分數的 `mid` 值:

scores["rouge1"].mid
Score(precision=0.86, recall=1.0, fmeasure=0.92)

太棒了,精確度和召回率數字匹配!那麼其他 ROUGE 分數呢?`rouge2` 衡量的是二元組(即詞對)之間的重疊,而 `rougeL` 和 `rougeLsum` 則透過查詢生成摘要和參考摘要中最長的共同子字串來衡量最長匹配序列的詞語。`rougeLsum` 中的“sum”指的是這個指標是針對整個摘要計算的,而 `rougeL` 則是按單個句子平均計算的。

✏️ 試試看! 建立你自己的生成摘要和參考摘要示例,看看生成的 ROUGE 分數是否與基於精確度和召回率公式的手動計算結果一致。作為加分項,將文字分割成二元組,並比較 `rouge2` 指標的精確度和召回率。

我們將使用這些 ROUGE 分數來跟蹤我們模型的效能,但在此之前,讓我們做每個優秀的 NLP 從業者都應該做的事情:建立一個強大而簡單的基線!

建立強大的基線

文字摘要的一個常見基線是簡單地提取文章的前三句話,通常稱為_lead-3_基線。我們可以使用句號來跟蹤句子邊界,但這在像“U.S.”或“U.N.”這樣的縮寫詞上會失敗——所以我們將使用`nltk`庫,它包含一個更好的演算法來處理這些情況。你可以使用`pip`安裝該軟體包,如下所示:

!pip install nltk

然後下載標點符號規則:

import nltk

nltk.download("punkt")

接下來,我們從 `nltk` 匯入句子分詞器,並建立一個簡單的函式來提取評論中的前三句話。文字摘要的約定是用換行符分隔每個摘要,所以我們也這樣做,並在訓練示例上進行測試。

from nltk.tokenize import sent_tokenize


def three_sentence_summary(text):
    return "\n".join(sent_tokenize(text)[:3])


print(three_sentence_summary(books_dataset["train"][1]["review_body"]))
'I grew up reading Koontz, and years ago, I stopped,convinced i had "outgrown" him.'
'Still,when a friend was looking for something suspenseful too read, I suggested Koontz.'
'She found Strangers.'

這似乎可行,所以現在讓我們實現一個函式,從資料集中提取這些“摘要”並計算基線的 ROUGE 分數。

def evaluate_baseline(dataset, metric):
    summaries = [three_sentence_summary(text) for text in dataset["review_body"]]
    return metric.compute(predictions=summaries, references=dataset["review_title"])

然後我們可以使用此函式來計算驗證集上的 ROUGE 分數,並使用 Pandas 對它們進行一些美化。

import pandas as pd

score = evaluate_baseline(books_dataset["validation"], rouge_score)
rouge_names = ["rouge1", "rouge2", "rougeL", "rougeLsum"]
rouge_dict = dict((rn, round(score[rn].mid.fmeasure * 100, 2)) for rn in rouge_names)
rouge_dict
{'rouge1': 16.74, 'rouge2': 8.83, 'rougeL': 15.6, 'rougeLsum': 15.96}

我們可以看到 `rouge2` 分數明顯低於其他分數;這可能反映了評論標題通常簡潔,因此 lead-3 基線過於冗長的事實。現在我們有了一個很好的基線可以開始工作,讓我們將注意力轉向微調 mT5!

使用 Trainer API 微調 mT5

微調用於摘要的模型與我們在本章中介紹的其他任務非常相似。我們需要做的第一件事是從 `mt5-small` 檢查點載入預訓練模型。由於摘要是一個序列到序列的任務,我們可以使用 `AutoModelForSeq2SeqLM` 類載入模型,該類將自動下載並快取權重。

from transformers import AutoModelForSeq2SeqLM

model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

💡 如果你好奇為什麼在下游任務上微調模型時沒有看到任何警告,那是因為對於序列到序列的任務,我們保留了網路的所有權重。與我們在第 3 章中的文字分類模型進行比較,在那裡預訓練模型的頭部被隨機初始化的網路替換了。

我們接下來需要做的就是登入 Hugging Face Hub。如果你在 notebook 中執行此程式碼,可以使用以下實用函式完成此操作:

from huggingface_hub import notebook_login

notebook_login()

這將顯示一個您可以在其中輸入憑據的視窗小部件。或者,您可以在終端中執行此命令並在此處登入。

huggingface-cli login

我們將需要在訓練期間生成摘要以計算 ROUGE 分數。幸運的是,🤗 Transformers 提供了專門的 `Seq2SeqTrainingArguments` 和 `Seq2SeqTrainer` 類,可以為我們自動完成此操作!為了瞭解其工作原理,我們首先定義實驗的超引數和其他引數:

from transformers import Seq2SeqTrainingArguments

batch_size = 8
num_train_epochs = 8
# Show the training loss with every epoch
logging_steps = len(tokenized_datasets["train"]) // batch_size
model_name = model_checkpoint.split("/")[-1]

args = Seq2SeqTrainingArguments(
    output_dir=f"{model_name}-finetuned-amazon-en-es",
    evaluation_strategy="epoch",
    learning_rate=5.6e-5,
    per_device_train_batch_size=batch_size,
    per_device_eval_batch_size=batch_size,
    weight_decay=0.01,
    save_total_limit=3,
    num_train_epochs=num_train_epochs,
    predict_with_generate=True,
    logging_steps=logging_steps,
    push_to_hub=True,
)

這裡,`predict_with_generate` 引數已設定為指示我們應在評估期間生成摘要,以便我們可以計算每個 epoch 的 ROUGE 分數。如第 1 章所述,解碼器透過逐個預測 token 來執行推理,這由模型的 `generate()` 方法實現。將 `predict_with_generate=True` 設定為告訴 `Seq2SeqTrainer` 使用該方法進行評估。我們還調整了一些預設超引數,如學習率、epoch 數量和權重衰減,並且我們設定了 `save_total_limit` 選項以在訓練期間僅儲存最多 3 個檢查點——這是因為即使是 mT5 的“小型”版本也使用大約 1 GB 的硬碟空間,我們可以透過限制儲存的副本數量來節省一些空間。

引數 `push_to_hub=True` 將允許我們在訓練後將模型推送到 Hub;您可以在 `output_dir` 定義的位置下的使用者配置檔案中找到該倉庫。請注意,您可以使用 `hub_model_id` 引數指定要推送到的倉庫名稱(特別是,您必須使用此引數才能推送到組織)。例如,當我們推送到 `huggingface-course` 組織時,我們將 `hub_model_id="huggingface-course/mt5-finetuned-amazon-en-es"` 新增到 `Seq2SeqTrainingArguments`。

接下來我們需要做的是為訓練器提供一個 `compute_metrics()` 函式,以便我們可以在訓練期間評估模型。對於摘要任務,這比簡單地在模型預測上呼叫 `rouge_score.compute()` 要複雜一些,因為我們需要先將輸出和標籤_解碼_成文字,然後才能計算 ROUGE 分數。以下函式正是這樣做的,並且還使用 `nltk` 的 `sent_tokenize()` 函式用換行符分隔摘要句子:

import numpy as np


def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    # Decode generated summaries into text
    decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
    # Replace -100 in the labels as we can't decode them
    labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
    # Decode reference summaries into text
    decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
    # ROUGE expects a newline after each sentence
    decoded_preds = ["\n".join(sent_tokenize(pred.strip())) for pred in decoded_preds]
    decoded_labels = ["\n".join(sent_tokenize(label.strip())) for label in decoded_labels]
    # Compute ROUGE scores
    result = rouge_score.compute(
        predictions=decoded_preds, references=decoded_labels, use_stemmer=True
    )
    # Extract the median scores
    result = {key: value.mid.fmeasure * 100 for key, value in result.items()}
    return {k: round(v, 4) for k, v in result.items()}

接下來,我們需要為我們的序列到序列任務定義一個數據整理器。由於 mT5 是一個編碼器-解碼器 Transformer 模型,準備批次時的一個微妙之處是,在解碼過程中我們需要將標籤向右移動一位。這是為了確保解碼器只看到先前的真實標籤,而不是當前或未來的標籤,否則模型很容易記住。這與在因果語言建模等任務中如何對輸入應用掩碼自注意力類似。

幸運的是,🤗 Transformers 提供了一個 `DataCollatorForSeq2Seq` 整理器,它將為我們動態填充輸入和標籤。要例項化這個整理器,我們只需提供 `tokenizer` 和 `model`:

from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

讓我們看看這個整理器在輸入一小批示例時會產生什麼。首先,我們需要刪除包含字串的列,因為整理器不知道如何填充這些元素。

tokenized_datasets = tokenized_datasets.remove_columns(
    books_dataset["train"].column_names
)

由於整理器期望一個 `dict` 列表,其中每個 `dict` 代表資料集中的一個示例,我們還需要在將其傳遞給資料整理器之前將資料整理成預期格式。

features = [tokenized_datasets["train"][i] for i in range(2)]
data_collator(features)
{'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'input_ids': tensor([[  1494,    259,   8622,    390,    259,    262,   2316,   3435,    955,
            772,    281,    772,   1617,    263,    305,  14701,    260,   1385,
           3031,    259,  24146,    332,   1037,    259,  43906,    305,    336,
            260,      1,      0,      0,      0,      0,      0,      0],
        [   259,  27531,  13483,    259,   7505,    260, 112240,  15192,    305,
          53198,    276,    259,  74060,    263,    260,    459,  25640,    776,
           2119,    336,    259,   2220,    259,  18896,    288,   4906,    288,
           1037,   3931,    260,   7083, 101476,   1143,    260,      1]]), 'labels': tensor([[ 7483,   259,  2364, 15695,     1,  -100],
        [  259, 27531, 13483,   259,  7505,     1]]), 'decoder_input_ids': tensor([[    0,  7483,   259,  2364, 15695,     1],
        [    0,   259, 27531, 13483,   259,  7505]])}

這裡要注意的主要一點是,第一個示例比第二個長,因此第二個示例的 `input_ids` 和 `attention_mask` 在右側填充了 `[PAD]` 令牌(其 ID 為 `0`)。同樣,我們可以看到 `labels` 已用 `-100` 填充,以確保損失函式忽略填充令牌。最後,我們可以看到一個新的 `decoder_input_ids`,它透過在第一個條目中插入一個 `[PAD]` 令牌將標籤向右移動了一位。

我們終於有了訓練所需的所有要素!現在我們只需使用標準引數例項化訓練器:

from transformers import Seq2SeqTrainer

trainer = Seq2SeqTrainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

並啟動我們的訓練執行。

trainer.train()

在訓練期間,您應該會看到訓練損失隨著每個 epoch 的增加而減少,ROUGE 分數也會增加。訓練完成後,您可以透過執行 `Trainer.evaluate()` 來檢視最終的 ROUGE 分數:

trainer.evaluate()
{'eval_loss': 3.028524398803711,
 'eval_rouge1': 16.9728,
 'eval_rouge2': 8.2969,
 'eval_rougeL': 16.8366,
 'eval_rougeLsum': 16.851,
 'eval_gen_len': 10.1597,
 'eval_runtime': 6.1054,
 'eval_samples_per_second': 38.982,
 'eval_steps_per_second': 4.914}

從分數中我們可以看出,我們的模型輕鬆超越了我們的 lead-3 基線——棒極了!最後要做的是將模型權重推送到 Hub,如下所示:

trainer.push_to_hub(commit_message="Training complete", tags="summarization")
'https://huggingface.co/huggingface-course/mt5-finetuned-amazon-en-es/commit/aa0536b829b28e73e1e4b94b8a5aacec420d40e0'

這會將檢查點和配置檔案儲存到 `output_dir`,然後將所有檔案上傳到 Hub。透過指定 `tags` 引數,我們還確保 Hub 上的小部件將是摘要管道的小部件,而不是與 mT5 架構關聯的預設文字生成小部件(有關模型標籤的更多資訊,請參閱 🤗 Hub 文件)。`trainer.push_to_hub()` 的輸出是 Git 提交雜湊的 URL,因此您可以輕鬆檢視對模型儲存庫所做的更改!

總結本節,讓我們看看如何使用 🤗 Accelerate 提供的低階功能來微調 mT5。

使用 🤗 Accelerate 微調 mT5

使用 🤗 Accelerate 微調我們的模型與我們在第 3 章中遇到的文字分類示例非常相似。主要區別在於需要顯式地在訓練期間生成摘要並定義如何計算 ROUGE 分數(回想一下 `Seq2SeqTrainer` 為我們處理了生成)。讓我們看看如何在 🤗 Accelerate 中實現這兩個要求!

為訓練做準備

我們需要做的第一件事是為每個分割建立一個 `DataLoader`。由於 PyTorch 資料載入器期望的是張量批次,我們需要在資料集中將格式設定為 `"torch"`。

tokenized_datasets.set_format("torch")

現在我們有了只包含張量的資料集,接下來要做的是再次例項化 `DataCollatorForSeq2Seq`。為此,我們需要提供一個全新的模型版本,所以讓我們從快取中再次載入它。

model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

然後我們可以例項化資料整理器並用它來定義我們的資料載入器。

from torch.utils.data import DataLoader

batch_size = 8
train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=batch_size,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=batch_size
)

接下來要做的就是定義我們想要使用的最佳化器。與我們的其他示例一樣,我們將使用 `AdamW`,它適用於大多數問題。

from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=2e-5)

最後,我們將模型、最佳化器和資料載入器提供給 `accelerator.prepare()` 方法。

from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)

🚨 如果您正在 TPU 上訓練,您需要將以上所有程式碼移到一個專用的訓練函式中。有關更多詳細資訊,請參閱第 3 章

現在我們已經準備好了物件,還有三件事要做:

  • 定義學習率排程。
  • 實現一個函式,用於對摘要進行後處理以進行評估。
  • 在 Hub 上建立一個倉庫,我們可以將我們的模型推送上去。

對於學習率排程,我們將使用之前章節的標準線性排程。

from transformers import get_scheduler

num_train_epochs = 10
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

對於後處理,我們需要一個函式,將生成的摘要分割成用換行符分隔的句子。這是 ROUGE 指標期望的格式,我們可以透過以下程式碼片段來實現:

def postprocess_text(preds, labels):
    preds = [pred.strip() for pred in preds]
    labels = [label.strip() for label in labels]

    # ROUGE expects a newline after each sentence
    preds = ["\n".join(nltk.sent_tokenize(pred)) for pred in preds]
    labels = ["\n".join(nltk.sent_tokenize(label)) for label in labels]

    return preds, labels

如果你還記得我們是如何定義 `Seq2SeqTrainer` 的 `compute_metrics()` 函式的,這應該看起來很熟悉。

最後,我們需要在 Hugging Face Hub 上建立一個模型倉庫。為此,我們可以使用標題恰當的 🤗 Hub 庫。我們只需要為我們的倉庫定義一個名稱,並且該庫有一個實用函式可以將倉庫 ID 與使用者個人資料結合起來。

from huggingface_hub import get_full_repo_name

model_name = "test-bert-finetuned-squad-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'lewtun/mt5-finetuned-amazon-en-es-accelerate'

現在我們可以使用這個倉庫名稱將本地版本克隆到我們的結果目錄,該目錄將儲存訓練工件。

from huggingface_hub import Repository

output_dir = "results-mt5-finetuned-squad-accelerate"
repo = Repository(output_dir, clone_from=repo_name)

這將允許我們在訓練期間呼叫 `repo.push_to_hub()` 方法將工件推回 Hub!現在讓我們透過編寫訓練迴圈來結束我們的分析。

訓練迴圈

摘要的訓練迴圈與我們遇到的其他 🤗 Accelerate 示例非常相似,大致分為四個主要步驟:

  1. 透過遍歷 `train_dataloader` 中每個 epoch 的所有示例來訓練模型。
  2. 在每個 epoch 結束時生成模型摘要,首先生成 token,然後將它們(和參考摘要)解碼為文字。
  3. 使用我們之前看到的相同技術計算 ROUGE 分數。
  4. 儲存檢查點並將所有內容推送到 Hub。這裡我們依賴 `Repository` 物件的巧妙的 `blocking=False` 引數,以便我們可以_非同步_地推送每個 epoch 的檢查點。這使得我們可以繼續訓練,而無需等待與 GB 大小模型相關的有些緩慢的上傳!

這些步驟可以在以下程式碼塊中看到:

from tqdm.auto import tqdm
import torch
import numpy as np

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # Training
    model.train()
    for step, batch in enumerate(train_dataloader):
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # Evaluation
    model.eval()
    for step, batch in enumerate(eval_dataloader):
        with torch.no_grad():
            generated_tokens = accelerator.unwrap_model(model).generate(
                batch["input_ids"],
                attention_mask=batch["attention_mask"],
            )

            generated_tokens = accelerator.pad_across_processes(
                generated_tokens, dim=1, pad_index=tokenizer.pad_token_id
            )
            labels = batch["labels"]

            # If we did not pad to max length, we need to pad the labels too
            labels = accelerator.pad_across_processes(
                batch["labels"], dim=1, pad_index=tokenizer.pad_token_id
            )

            generated_tokens = accelerator.gather(generated_tokens).cpu().numpy()
            labels = accelerator.gather(labels).cpu().numpy()

            # Replace -100 in the labels as we can't decode them
            labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
            if isinstance(generated_tokens, tuple):
                generated_tokens = generated_tokens[0]
            decoded_preds = tokenizer.batch_decode(
                generated_tokens, skip_special_tokens=True
            )
            decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

            decoded_preds, decoded_labels = postprocess_text(
                decoded_preds, decoded_labels
            )

            rouge_score.add_batch(predictions=decoded_preds, references=decoded_labels)

    # Compute metrics
    result = rouge_score.compute()
    # Extract the median ROUGE scores
    result = {key: value.mid.fmeasure * 100 for key, value in result.items()}
    result = {k: round(v, 4) for k, v in result.items()}
    print(f"Epoch {epoch}:", result)

    # Save and upload
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )
Epoch 0: {'rouge1': 5.6351, 'rouge2': 1.1625, 'rougeL': 5.4866, 'rougeLsum': 5.5005}
Epoch 1: {'rouge1': 9.8646, 'rouge2': 3.4106, 'rougeL': 9.9439, 'rougeLsum': 9.9306}
Epoch 2: {'rouge1': 11.0872, 'rouge2': 3.3273, 'rougeL': 11.0508, 'rougeLsum': 10.9468}
Epoch 3: {'rouge1': 11.8587, 'rouge2': 4.8167, 'rougeL': 11.7986, 'rougeLsum': 11.7518}
Epoch 4: {'rouge1': 12.9842, 'rouge2': 5.5887, 'rougeL': 12.7546, 'rougeLsum': 12.7029}
Epoch 5: {'rouge1': 13.4628, 'rouge2': 6.4598, 'rougeL': 13.312, 'rougeLsum': 13.2913}
Epoch 6: {'rouge1': 12.9131, 'rouge2': 5.8914, 'rougeL': 12.6896, 'rougeLsum': 12.5701}
Epoch 7: {'rouge1': 13.3079, 'rouge2': 6.2994, 'rougeL': 13.1536, 'rougeLsum': 13.1194}
Epoch 8: {'rouge1': 13.96, 'rouge2': 6.5998, 'rougeL': 13.9123, 'rougeLsum': 13.7744}
Epoch 9: {'rouge1': 14.1192, 'rouge2': 7.0059, 'rougeL': 14.1172, 'rougeLsum': 13.9509}

就是這樣!一旦你執行這個,你將擁有一個模型和結果,它們與我們使用 `Trainer` 獲得的結果非常相似。

使用微調模型

將模型推送到 Hub 後,您可以透過推理小部件或使用 `pipeline` 物件來使用它,如下所示:

from transformers import pipeline

hub_model_id = "huggingface-course/mt5-small-finetuned-amazon-en-es"
summarizer = pipeline("summarization", model=hub_model_id)

我們可以從測試集中(模型未見過)輸入一些示例到我們的管道中,以瞭解摘要的質量。首先讓我們實現一個簡單的函式來同時顯示評論、標題和生成的摘要:

def print_summary(idx):
    review = books_dataset["test"][idx]["review_body"]
    title = books_dataset["test"][idx]["review_title"]
    summary = summarizer(books_dataset["test"][idx]["review_body"])[0]["summary_text"]
    print(f"'>>> Review: {review}'")
    print(f"\n'>>> Title: {title}'")
    print(f"\n'>>> Summary: {summary}'")

我們來看看其中一個英文例子:

print_summary(100)
'>>> Review: Nothing special at all about this product... the book is too small and stiff and hard to write in. The huge sticker on the back doesn’t come off and looks super tacky. I would not purchase this again. I could have just bought a journal from the dollar store and it would be basically the same thing. It’s also really expensive for what it is.'

'>>> Title: Not impressed at all... buy something else'

'>>> Summary: Nothing special at all about this product'

這還不錯!我們可以看到我們的模型實際上已經能夠透過用新詞語補充評論的一部分來執行_抽象_摘要。我們模型最酷的方面也許是它是雙語的,所以我們也可以生成西班牙語評論的摘要:

print_summary(0)
'>>> Review: Es una trilogia que se hace muy facil de leer. Me ha gustado, no me esperaba el final para nada'

'>>> Title: Buena literatura para adolescentes'

'>>> Summary: Muy facil de leer'

摘要翻譯成英文是“Very easy to read”(非常容易閱讀),在本例中我們可以看到這是直接從評論中提取的。儘管如此,這展示了 mT5 模型的通用性,並讓您體驗了處理多語言語料庫的感覺!

接下來,我們將把注意力轉向一個稍微複雜的任務:從頭開始訓練一個語言模型。

< > 在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.