並獲得增強文件體驗
開始使用
摘要
在本節中,我們將瞭解如何使用 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_datasetDatasetDict({
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”的示例資訊量不大,但其他標題看起來像是對評論本身的不錯摘要。 在所有 400,000 條評論上訓練摘要模型將花費太長時間才能在一個 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 個詞
為了解決這個問題,我們將過濾掉標題非常短的示例,以便我們的模型能夠生成更有意義的摘要。 由於我們正在處理英語和西班牙語文字,因此我們可以使用粗略的啟發式方法在空格處分割標題,然後使用我們可靠的 Dataset.filter() 方法,如下所示
books_dataset = books_dataset.filter(lambda x: len(x["review_title"].split()) > 2)現在我們已經準備好了語料庫,讓我們看看一些可以在其上進行微調的可能的 Transformer 模型!
文字摘要模型
仔細想想,文字摘要與機器翻譯的任務類似:我們有一段文字,例如評論,希望將其“翻譯”成一個較短的版本,並保留輸入文字的關鍵特徵。因此,大多數用於摘要的 Transformer 模型都採用了我們在第 1 章中首次遇到的編碼器-解碼器架構,但也有一些例外,例如 GPT 系列模型,它也可以在少樣本設定中用於摘要。下表列出了一些可用於微調摘要的流行預訓練模型。
| Transformer 模型 | 描述 | 支援多語言? |
|---|---|---|
| GPT-2 | 雖然它是作為自迴歸語言模型進行訓練的,但可以透過在輸入文字末尾附加“TL;DR”來使 GPT-2 生成摘要。 | ❌ |
| PEGASUS | 使用預訓練目標來預測多句文字中的掩碼句子。這種預訓練目標比普通的語言建模更接近於摘要,並且在流行的基準測試中得分很高。 | ❌ |
| T5 | 一種通用的 Transformer 架構,它將所有任務都表述為文字到文字的框架;例如,模型總結文件的輸入格式為 summarize: ARTICLE。 | ❌ |
| mT5 | T5 的多語言版本,在涵蓋 101 種語言的多語言公共爬蟲語料庫 (mC4) 上進行預訓練。 | ✅ |
| BART | 一種新穎的 Transformer 架構,它同時具有編碼器和解碼器堆疊,經過訓練以重建損壞的輸入,並結合了 BERT 和 GPT-2 的預訓練方案。 | ❌ |
| mBART-50 | BART 的多語言版本,在 50 種語言上進行預訓練。 | ✅ |
從該表中可以看出,大多數用於摘要的 Transformer 模型(實際上大多數 NLP 任務)都是單語的。如果您的任務是使用“高資源”語言(如英語或德語),那麼這很好,但對於世界上使用的其他數千種語言來說,就不那麼好了。幸運的是,有一類多語言 Transformer 模型,如 mT5 和 mBART,可以解決這個問題。這些模型使用語言建模進行預訓練,但帶有一個變化:它們不是在一個語言的語料庫上進行訓練,而是在 50 多種語言的文字上同時進行訓練!
我們將重點關注 mT5,這是一個基於 T5 的有趣架構,它是在文字到文字框架中進行預訓練的。在 T5 中,每個 NLP 任務都用類似於 summarize: 的提示字首來表述,這使模型能夠根據提示調整生成的文字。如下圖所示,這使得 T5 非常通用,因為您可以使用單個模型解決許多工!
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 字元 和序列結束標記 </s> 表明我們正在處理 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。這將示例編碼成 1000 個批次(預設值),並允許您利用 🤗 Transformers 中快速標記器的多執行緒功能。在可能的情況下,嘗試使用 batched=True 以充分利用您的預處理!
文字摘要指標
與我們在本課程中介紹的大多數其他任務相比,衡量文字生成任務(如摘要或翻譯)的效能並不那麼簡單。例如,給定一個評論“我非常喜歡讀飢餓遊戲”,有多個有效的摘要,例如“我喜歡飢餓遊戲”或“飢餓遊戲是一本很棒的書”。顯然,在生成的摘要和標籤之間應用某種形式的完全匹配不是一個好的解決方案——即使是人類在這種指標下也會表現不佳,因為我們每個人都有自己的寫作風格。
對於摘要,最常用的指標之一是ROUGE 分數(摘錄評估的召回導向研究的縮寫)。該指標的基本思想是將生成的摘要與一組通常由人類建立的參考摘要進行比較。為了更精確,假設我們要比較以下兩個摘要
generated_summary = "I absolutely loved reading the Hunger Games"
reference_summary = "I loved reading the Hunger Games"比較它們的一種方法是計算重疊單詞的數量,在本例中為 6 個。但是,這有點粗略,因此 ROUGE 基於計算重疊的精確率和召回率分數。
🙋 如果您第一次聽說精確率和召回率,請不要擔心——我們將一起瀏覽一些明確的示例,以使其變得清晰。這些指標通常出現在分類任務中,因此,如果您想了解在該上下文中如何定義精確率和召回率,我們建議您檢視 scikit-learn 的指南。
對於 ROUGE,召回率衡量的是生成的摘要捕獲了多少參考摘要。如果我們只是比較單詞,則可以根據以下公式計算召回率:
對於我們上面簡單的例子,這個公式給出了完美的召回率 6/6 = 1;也就是說,參考摘要中的所有單詞都由模型生成。這聽起來很棒,但想象一下,如果我們生成的摘要是“我真的很喜歡整晚閱讀飢餓遊戲”。這也會有完美的召回率,但可以說是一個更糟糕的摘要,因為它冗長。為了處理這些情況,我們還計算了精確率,在 ROUGE 上下文中,精確率衡量了生成摘要中相關部分的多少。
將此應用於我們的冗長摘要,得到精確率為 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 變體是單字詞的重疊——這只是說單詞的重疊的一種花哨說法,並且正是我們上面討論過的指標。為了驗證這一點,讓我們提取分數的 mid 值
scores["rouge1"].midScore(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。如果你在筆記本中執行此程式碼,可以使用以下實用程式函式
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 章所述,解碼器透過逐個預測標記來執行推理,這由模型的 generate() 方法實現。將 predict_with_generate=True 設定為告訴 Seq2SeqTrainer 使用該方法進行評估。我們還調整了一些預設的超引數,例如學習率、epoch 數量和權重衰減,並將 save_total_limit 選項設定為僅在訓練期間儲存最多 3 個檢查點——這是因為即使是 mT5 的“小型”版本也使用了大約 1GB 的硬碟空間,我們可以透過限制儲存的副本數量來節省一些空間。
push_to_hub=True 引數將允許我們在訓練後將模型推送到 Hub;您將在 output_dir 定義的位置的使用者配置檔案下找到儲存庫。請注意,您可以使用 hub_model_id 引數指定要推送到哪個儲存庫的名稱(特別是,您必須使用此引數才能推送到組織)。例如,當我們將模型推送到huggingface-course 組織時,我們在 Seq2SeqTrainingArguments 中添加了 hub_model_id="huggingface-course/mt5-finetuned-amazon-en-es"。
接下來我們需要做的是為訓練器提供一個 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 分數隨著每個 epoch 而增加。訓練完成後,您可以透過執行 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}從分數中我們可以看出,我們的模型輕鬆地超越了我們的前 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 示例非常相似,大致分為四個主要步驟
- 透過迭代每個 epoch 中
train_dataloader中的所有示例來訓練模型。 - 在每個 epoch 結束時生成模型摘要,方法是首先生成標記,然後將它們(以及參考摘要)解碼為文字。
- 使用我們之前看到的相同技術計算 ROUGE 分數。
- 儲存檢查點並將所有內容推送到 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'該摘要翻譯成英語為“非常易讀”,我們可以看到在這種情況下它是直接從評論中提取的。儘管如此,這還是展示了mT5模型的多功能性,並讓您體驗了處理多語言語料庫的感覺!
接下來,我們將注意力轉向一項稍微複雜的任務:從頭開始訓練語言模型。