隨著程式碼自動補全工具的興起,利用 Transformer 模型生成程式碼成為一個熱門研究方向。本文將引導讀者建立一個名為 CodeParrot 的 GPT-like 模型,專門用於生成 Python 程式碼。不同於以往聚焦於資料受限場景的遷移學習方法,本章將探討如何利用大型資料集從零開始訓練模型,並深入研究預訓練步驟、分散式訓練技巧以及資料集的建立與處理。過程中將使用 Accelerate 函式庫進行多 GPU 訓練,並探討如何收集、處理龐大的資料集、建立自定義分詞器等議題,最終目標是讓讀者掌握訓練大型程式碼生成模型的完整流程。
從零開始訓練Transformer:開發CodeParrot模型
在這本文的開篇,我們提到了GitHub Copilot這樣一個先進的應用程式,它使用類別似GPT的Transformer模型來實作程式碼自動補全功能。對於初學者或是正在學習新語言、新框架的開發者來說,這項功能尤其有用,因為它能夠自動生成範本程式碼。除此之外,TabNine和Kite等產品也使用了AI模型來達到相同的目的。在第5章中,我們探討瞭如何使用GPT模型來生成高品質的文字。在本章中,我們將進一步構建自己的GPT-like模型,用於生成Python原始碼,並將其命名為CodeParrot。
迄今為止,我們主要關注的是資料受限的應用場景,在這些場景中,標註好的訓練資料數量有限。在這種情況下,遷移學習幫助我們建立了效能良好的模型。我們在第9章中將遷移學習發揮到了極致,幾乎沒有使用任何訓練資料。在本章中,我們將轉向另一個極端,探討當我們擁有大量資料時可以做些什麼。我們將深入研究預訓練步驟本身,並學習如何從零開始訓練一個Transformer模型。在解決這個問題的過程中,我們將探討一些之前未曾考慮過的訓練導向,例如:
- 收集和處理龐大的資料集
- 為我們的資料集建立自定義的分詞器
- 在多個GPU上大規模訓練模型
為了高效地訓練具有數十億引數的大型模型,我們需要特殊的分散式訓練工具。雖然Transformers中的Trainer支援分散式訓練,但我們將藉此機會展示一個強大的PyTorch函式庫——Accelerate。我們最終將接觸到當今使用的一些最大的NLP模型,但首先,我們需要找到一個足夠大的資料集。
與本文中的其他程式碼不同(可以在單個GPU上使用Jupyter notebook執行),本章中的訓練程式碼設計為使用多個GPU以指令碼形式執行。如果您希望訓練自己的CodeParrot版本,我們建議執行Transformers儲存函式庫中提供的指令碼。
大型資料集及其來源
在許多領域中,您可能實際上擁有大量的資料,從法律檔案到生物醫學資料集,再到程式碼函式庫。在大多數情況下,這些資料集都是未標註的,而且由於其龐大的規模,通常只能透過使用啟發式方法或在收集過程中儲存的相關元資料來進行標註。
儘管如此,即使大型語料函式庫未標註或僅透過啟發式方法標註,它仍然非常有用。我們在第9章中看到了這樣一個例子,在那裡我們使用了資料集中未標註的部分來微調語言模型,以適應特定領域。當可用的資料有限時,這種方法通常會帶來效能上的提升。決定從零開始訓練模型還是微調現有的模型,主要取決於您用於微調的語料函式庫的大小,以及可用的預訓練模型與語料函式庫之間的領域差異。
使用預訓練模型會強制您使用該模型對應的分詞器,但使用在其他領域語料函式庫上訓練的分詞器通常不是最佳選擇。例如,在法律檔案、其他語言,甚至是完全不同的序列(如音樂符號或DNA序列)上使用GPT的預訓練分詞器,將導致不良的分詞效果(正如我們很快就會看到的)。
當您擁有的訓練資料量接近用於預訓練的資料量時,如果具備必要的計算資源,那麼從零開始訓練模型和分詞器就變得非常有意義。在討論不同的預訓練目標之前,我們首先需要建立一個適合預訓練的大型語料函式庫。建立這樣一個語料函式庫伴隨著它自己的挑戰,我們將在下一節中探討。
建立大型語料函式庫的挑戰
預訓練後模型的品質在很大程度上反映了預訓練語料函式庫的品質。特別是,模型將繼承預訓練語料函式庫中的任何缺陷。因此,在嘗試建立一個之前,瞭解與建立大型語料函式庫相關的一些常見問題和挑戰是非常重要的。
隨著資料集越來越大,您完全控制或至少精確瞭解其中內容的可能性會降低。一個非常大的資料集很可能不是由專門的建立者精心製作的,他們會意識到並瞭解整個流程和機器學習模型將要應用的任務。相反,更有可能的是,一個非常大的資料集是以自動或半自動的方式建立的,透過收集作為其他活動副產品的資料。例如,它可能包含公司儲存的所有檔案(如契約、採購訂單等)、使用者活動日誌,或從網際網路收集的資料。
大型資料集大多是以高度自動化的方式建立的,這一事實帶來了幾個重要的後果。一個重要的考慮是,對其內容和建立方式的控制有限,因此訓練模型時面臨有偏見和低品質資料的風險增加。最近對著名的BERT和T5所使用的BookCorpus和C4等大型資料集的研究發現:
- C4語料函式庫中有相當一部分是機器翻譯的,而不是由人類翻譯的。
- C4中由於停用詞過濾導致非裔美國人英語被不同程度地抹殺,導致這種內容被低估。
- 在大型文字語料函式庫中,通常很難在包含(往往太多)性或其他露骨內容和完全抹殺所有對性或性的提及之間找到中間立場。其結果是,像“sex”這樣一個相當常見的詞(它既有中性和露骨的意思)在C4上訓練的分詞器中是完全未知的,因為這個詞在語料函式庫中完全不存在。
- BookCorpus中有許多侵權行為,其他大型資料集可能也是如此。
- BookCorpus中有浪漫小說的型別偏差。
這些發現可能與在這些語料函式庫上訓練的模型的下游使用並不相容。例如,浪漫小說的嚴重過度代表可能會對模型的效能產生負面影響,特別是在處理其他型別文字時。
內容解密:
本文主要討論了建立大型語料函式庫所面臨的挑戰,包括對資料內容和建立方式的控制有限,從而增加了使用有偏見和低品質資料進行模型訓練的風險。同時,也提到了一些著名大型資料集中的問題,例如C4和BookCorpus中的問題。
下一步
在本章中,我們將繼續探討如何從零開始訓練一個Transformer模型,用於生成Python原始碼,並深入研究預訓練步驟本身。同時,我們也將介紹如何使用Accelerate函式庫進行分散式訓練,以高效地訓練具有數十億引數的大型模型。接下來,我們將討論如何建立一個適合預訓練的大型語料函式庫,以及如何為我們的資料集建立自定義的分詞器。
大型文字資料集的挑戰與自建程式碼資料集的方法
文字資料集的偏差與模型訓練
在訓練大語言模型時,資料集的選擇對模型的表現和偏好有著重要的影響。以GPT和GPT-2為例,兩者雖然都是根據Transformer架構的語言模型,但由於訓練資料的不同,它們在文字生成任務上表現出不同的風格。GPT主要在BookCorpus上進行訓練,這使得它在生成文字時傾向於產生具有浪漫色彩的對話,而GPT-2則是在網路文字、部落格和新聞文章上進行訓練,生成文字的風格更為中性和多樣。
比較GPT和GPT-2的文字生成能力
from transformers import pipeline, set_seed
generation_gpt = pipeline("text-generation", model="openai-gpt")
generation_gpt2 = pipeline("text-generation", model="gpt2")
def model_size(model):
return sum(t.numel() for t in model.parameters())
print(f"GPT size: {model_size(generation_gpt.model)/1000**2:.1f}M parameters")
print(f"GPT2 size: {model_size(generation_gpt2.model)/1000**2:.1f}M parameters")
def enum_pipeline_ouputs(pipe, prompt, num_return_sequences):
out = pipe(prompt, num_return_sequences=num_return_sequences, clean_up_tokenization_spaces=True)
return "\n".join(f"{i+1}." + s["generated_text"] for i, s in enumerate(out))
prompt = "\nWhen they came back"
print("GPT completions:\n" + enum_pipeline_ouputs(generation_gpt, prompt, 3))
print("")
print("GPT-2 completions:\n" + enum_pipeline_ouputs(generation_gpt2, prompt, 3))
內容解密:
- 模型載入與比較:首先,我們使用
transformers函式庫中的pipeline函式載入了GPT和GPT-2兩個模型,並定義了一個函式model_size來計算模型的引數數量。 - 文字生成:透過定義
enum_pipeline_ouputs函式,我們能夠使用載入的模型生成文字,並將結果列舉出來。給定相同的輸入提示,GPT和GPT-2生成了不同的文字,這反映了它們訓練資料的不同。 - 結果分析:從生成的文字中可以看出,GPT傾向於生成具有浪漫色彩的對話,而GPT-2的生成結果則更為多樣和中性。
自建Python程式碼資料集
為了建立一個Python程式碼生成的模型,我們需要一個大型的Python原始碼資料集。GitHub是一個極佳的資源,因為它包含了大量的開原始碼倉函式庫。
使用Google BigQuery提取GitHub上的Python程式碼
- 建立Google Cloud帳戶:首先,需要建立一個Google Cloud帳戶,利用免費試用服務即可滿足需求。
- 建立BigQuery專案:在Google Cloud帳戶下建立一個BigQuery專案,並在專案中建立一個資料集。
- 執行SQL查詢:使用特定的SQL查詢陳述式,在
github_repos資料集中提取Python檔案。
-- 示例SQL查詢,用於提取GitHub上的Python檔案
SELECT * FROM `bigquery-public-data.github_repos.contents`
WHERE content LIKE '%Python%' AND sample_path LIKE '%.py';
內容解密:
- 資料提取:利用Google BigQuery的強大查詢功能,可以高效地從GitHub的開源倉函式庫中提取所需的Python程式碼檔案。
- 查詢陳述式:示例SQL查詢展示瞭如何根據特定條件(如檔案型別為
.py)篩選資料。 - 結果儲存:查詢結果可以儲存到指定的表中,方便進一步處理和下載。
大型資料集的處理與挑戰
在處理人工智慧與機器學習專案時,資料集的大小與品質往往決定了模型的表現與穩定性。特別是在訓練大語言模型或進行複雜的資料分析時,如何有效處理與管理龐大的資料整合為了一項重要的挑戰。本文將探討在處理大型資料集時所面臨的挑戰,以及如何利用現有的工具與技術克服這些挑戰。
從 GitHub BigQuery 資料集中提取 Python 程式碼
首先,我們需要從 GitHub BigQuery 資料集中提取 Python 程式碼。這個過程涉及執行一個 SQL 查詢,該查詢會聯接多個表格以取得所需的資料。查詢如下:
SELECT
f.repo_name, f.path, c.copies, c.size, c.content, l.license
FROM
`bigquery-public-data.github_repos.files` AS f
JOIN
`bigquery-public-data.github_repos.contents` AS c
ON
f.id = c.id
JOIN
`bigquery-public-data.github_repos.licenses` AS l
ON
f.repo_name = l.repo_name
WHERE
NOT c.binary
AND ((f.path LIKE '%.py')
AND (c.size BETWEEN 1024 AND 1048575))
內容解密:
此 SQL 查詢用於從 GitHub BigQuery 資料集中提取 Python 檔案的內容。查詢聯接了 files、contents 和 licenses 三個表格,分別取得檔案的儲存函式庫名稱、路徑、大小、內容和授權資訊。條件篩選確保只提取非二進位且大小在 1KB 至 1MB 之間的 Python 檔案。
這個查詢處理了大約 2.6 TB 的資料,提取了 2680 萬個檔案,最終得到了一個約 50 GB 的壓縮 JSON 檔案資料集。接下來,需要將這個資料集下載到本地機器上。
下載與儲存大型資料集
下載大型資料集需要足夠的頻寬和磁碟空間。可以透過以下兩步過程實作:
將結果匯出到 Google Cloud Storage(GCS):
- 在 GCS 中建立一個儲存桶和資料夾。
- 將查詢結果匯出到此儲存桶,格式選擇 JSON 並使用 gzip 壓縮。
使用
gsutil函式庫下載儲存桶到本地機器:- 安裝
gsutil。 - 使用 Google 帳戶組態
gsutil。 - 使用命令
$ gsutil -m -o "GSUtil:parallel_process_count=1" cp -r gs://<name_of_bucket>下載儲存桶。
- 安裝
或者,也可以直接從 Hugging Face Hub 使用 $ git clone https://huggingface.co/datasets/transformersbook/codeparrot 命令下載資料集。
處理噪音資料
由於任何人都可以建立 GitHub 儲存函式庫,因此專案品質參差不齊。在準備訓練資料集時,需要決定是否容忍一定的噪音,以使模型在真實世界環境中更具魯棒性,但同時也可能使預測結果更隨機。
對於教育目的,可以選擇不根據星級或使用情況過濾資料,而是直接抓取 GitHub BigQuery 資料集中的所有 Python 檔案。然而,資料準備是一個至關重要的步驟,需要盡可能清理資料集。
使用 Datasets 處理大型資料集
載入非常大的資料集是一項挑戰,尤其是當資料量超過機器的 RAM 時。幸運的是,Datasets 函式庫提供了兩種功能來克服這個問題:記憶體對映和串流。
記憶體對映
Datasets 使用預設啟用的零複製和零開銷記憶體對映機制。基本上,每個資料集都被快取在硬碟上的檔案中,這個檔案直接反映了 RAM 中的內容。透過設定 delete_extracted=True,可以在下載組態中確保刪除不再需要的檔案。
from datasets import load_dataset, DownloadConfig
download_config = DownloadConfig(delete_extracted=True)
dataset = load_dataset("./codeparrot", split="train", download_config=download_config)
內容解密:
這段程式碼用於載入本地儲存的 “codeparrot” 資料集,並設定下載組態以在提取後刪除不再需要的檔案。這樣可以節省磁碟空間。
載入後,可以檢查資料集的大小和所使用的 RAM:
import psutil
print(f"Number of python files code in dataset : {len(dataset)}")
ds_size = sum(os.stat(f["filename"]).st_size for f in dataset.cache_files)
print(f"Dataset size (cache file) : {ds_size / 2**30:.2f} GB")
print(f"RAM used: {psutil.Process(os.getpid()).memory_info().rss >> 20} MB")
內容解密:
這段程式碼輸出資料集中的 Python 檔案數量、快取檔案的大小(單位:GB)以及當前程式所使用的 RAM(單位:MB)。
處理大型資料集與資料串流技術
在處理龐大的資料集時(例如達到1 TB或以上),即使是標準硬碟也難以容納。此時,除了擴充伺服器組態外,資料串流(Streaming)技術成為另一種可行的解決方案。Hugging Face的Datasets函式庫支援多種壓縮或未壓縮的檔案格式,如JSON Lines、CSV或純文字檔案(無論是原始檔案還是經過zip、gzip或zstandard壓縮),都可以進行串流處理。
使用串流技術載入資料集
讓我們直接從壓縮的JSON檔案載入資料集,而不先建立快取檔案:
streamed_dataset = load_dataset('./codeparrot', split="train", streaming=True)
內容解密:
load_dataset函式用於載入資料集。./codeparrot是資料集的路徑。split="train"指定載入訓練集。streaming=True啟用串流模式。
在串流模式下,壓縮的JSON檔案會在需要時被開啟和讀取。我們的資料集現在變成了一個Iterable Dataset物件,這意味著我們無法直接存取其中的任意元素,如streamed_dataset[1264],而是需要按順序讀取,例如使用next(iter(streamed_dataset))。
串流資料集的特性
雖然無法直接存取任意元素,但我們仍然可以使用諸如shuffle()之類別的方法。不過,這些方法會透過擷取一個範例緩衝區並在其中進行洗牌來操作(緩衝區大小可調)。當有多個原始檔案時,shuffle()也會在迭代過程中隨機化檔案的順序。
串流技術的優勢
使用串流資料集的主要優勢在於,它不會在硬碟上建立快取檔案,也不需要太多的RAM記憶體。原始檔案會在需要新的範例批次時被提取和讀取,並且只有該批次會被載入記憶體。這樣一來,我們的資料集記憶體佔用量可以從180 GB減少到50 GB。
從Hugging Face Hub直接載入串流資料集
我們甚至可以直接從Hugging Face Hub參考資料集,然後在不下載原始檔案的情況下直接下載範例:
remote_dataset = load_dataset('transformersbook/codeparrot', split="train", streaming=True)
內容解密:
load_dataset函式同樣用於載入資料集,但這次是從Hugging Face Hub。'transformersbook/codeparrot'是Hub上的資料集名稱。split="train"和streaming=True的作用與之前相同。
這個資料集的行為與之前本地的串流資料集完全相同,但在背後,它會動態下載範例。有了這種設定,我們就可以在(幾乎)任意小的伺服器上使用任意大的資料集。