Stanford CS336 Day 1 - Overview, Tokenization

原文寫在筆者的Notion上:Notion

今日花費時數:6.5
進度:1. Overview, tokenization

前言

會開始這個挑戰是因為筆者處於有點頹廢的狀態已經好幾週了,再加上為了Data engineering Zoomcamp去申請的GCP免費試用額度5/12就要到期,一直在煩惱著要怎麼盡可能在過期前把額度用掉,而後就想起來了CS336。這門課因為需要極大的工作量 (參考Lecture 1: Comment from Spring 2024 course evaluation: The entire assignment was approximately the same amount of work as all 5 assignments from CS 224n plus the final project. And that’s just the first homework assignment.),再加上自學者通常還要為training model支付一些cloud的費用(課程頁面有提供一些較便宜的選項),因此筆者本來是打算至少等到2027再來學習的。但既然GCP有現成的免費額度沒地方使用,再加上筆者最近很希望能夠盡快回到隨時都可以保持高度專注的狀態,因此決定開始挑戰這門難度最高的Stanford課程之一。

筆記

筆者僅會將比較重視的內容寫到筆記中,如果想完全了解課程的內容,還是建議親自去看課程影片和教材

課程架構

這門課在設計上把重心放在 Efficiency drives design decisions,因為這是現在設計及訓練LLM最關鍵的思維:如何在有限的資源(data + hardware)下訓練出最佳的LLM?

Efficiency drives design decisions

我們目前處於計算資源受限的時代,因此大多數的設計決策主要反映了我們如何盡可能充分運用現有的硬體

  • Data processing: garbage in garbage out, data一直都是影響model quality的最重要因素。訓練LLM的成本相當高昂,因此避免在training model時把珍貴的算力資源浪費在品質不佳或不相關的data上非常重要。
  • Tokenization: tokenization strategy必須能夠配合現在的model architectures達到efficient computation。直接處理raw bytes雖然優雅,但會導致序列過長,在 Transformer 的 attention mechanism 時間複雜度為 O(n^2) 的特性下,會引發嚴重的計算效率災難。
  • Model architecture: 現在許多模型架構 (尤其是Transformer的variants) 的設計目的除了減少記憶體的使用量或是減少每秒浮點運算次數(FLOPs),更希望 minimize 硬體上極其昂貴的 data movement costs。
  • Training: 假設我們擁有大量data,計算資源卻很有限的話,應該使用全部資料訓練1 epoch。在資源有限的狀況下,使用重複的data訓練多個epochs帶來的效果遠不如只訓練1 epoch,但是盡可能將model暴露在更多的unseen data上。當然,這點不適用於計算資源充足的Lab (這些Lab通常缺乏的是data)。
  • Scaling laws: 訓練大模型是很昂貴的,因此直接在大模型上做hyperparameter tuning是相當浪費計算資源的行為。更好的做法是先在小模型上進行訓練及一些實驗,透過scaling law將這些實驗結果fit出一條scaling curve後,預測大模型在目前configuration下的performance。
  • Alignment: 在pre-training階段訓練出的foundation models,僅僅是針對預測下一個token來做訓練。在post-training階段還需要透過alignment使model能夠進一步遵守特定的指令以及拒絕有害的請求。假如我們希望將模型使用在非常特定的場景上,那通常只需要在較小的模型上做alignment和fine-tuning就能收到很好的效果了。 (但如果需要將模型運用在廣泛的場景上,訓練大模型依舊無可避免)

根據這些design decisions,課程被分為以下5個module

關於各Module更詳細的概述可以參考 lecture_01.py,筆者這裡就不詳細說明了,也有可能有心情的時候補上一部份

Tokenization

Tokenization幾乎是NLP領域一切的起點 - 也是編寫程式碼時最無聊且讓人痛苦的部分

Introduction

在電腦中,我們通常以Unicode strings的形式來表示文字,但language model是對tokens sequences做probability distribution modeling。因此,我們需要將strings encode為tokens的方法,以及將tokens還原為strings(decode)的方法。在language model中負責這兩項任務的module被稱為tokenizer,而vocabulary size 代表tokenizer所有可能的token數量 (integers)

具體來說,tokenizer 的運作本質上是依賴一張「詞彙表(Vocabulary)」。當 tokenizer 將原始字串切分後,會去查表將每個 token 映射(map)並轉換為對應的 integer indicies,這個過程稱為 encode;而 decode 則是將模型輸出的數字序列,根據同一張對照表反向查詢,轉換回人類可讀的strings

由於tokenization的方法會直接決定一段文本以多長的sequence餵給model,因此提升compression ratio,也就是”每個token能代表多少Bytes的資訊”非常重要。下方的function為計算compression ratio的方式。以GPT-2為例,其compression ratio大約落在1.6左右。

def get_compression_ratio(string: str, indices: list[int]) -> float:
    """Given `string` that has been tokenized into `indices`, ."""
    num_bytes = len(bytes(string, encoding="utf-8"))  # @inspect num_bytes
    num_tokens = len(indices)                       # @inspect num_tokens
    return num_bytes / num_tokens

Character-based tokenization

前面提到在電腦中通常以Unicode strings表示文字,而Unicode strings其實就是一連串的Unicode characters,因此我們可以將Unicode strings切割為Unicode characters後,再將每個character轉換為code point (integer)

Unicode characters 大約有150000個,因此使用這個方法顯然會帶來一些問題

  1. Tokenizer的vocabulary (詞彙表) 太大了
  2. 很多characters實際上很少出現(例如:globe_showing_europe_africa:), 這意味著我們會有一張使用效率極低的vocabulary

下方是character-based tokenization的toy implementation

class CharacterTokenizer:
    """Represent a string as a sequence of Unicode code points."""
    def encode(self, string: str) -> list[int]:
        return list(map(ord, string))
    def decode(self, indices: list[int]) -> str:
        return "".join(map(chr, indices))

Byte-based tokenization

Unicode strings可以表示為一系列的bytes,而每個byte又可以用數字0~255表示,因此可以透過將Unicode strings轉為bytes (最常見的encoding方式為UTF-8) 來做tokenization。(這裡要注意的是,1個character ≠ 1個byte,有些character需要多個bytes才能表示)

這個方法的優點在於我們的vocabulary size只有256!然而代價就是,這個方法的compression ratio為1,這會導致餵給model的sequence太長。在現在LLM以Transformer架構為主的情況下,由於context length是有限制的(attention的時間複雜度是 O(n^2) ),這個方法會直接大幅減少LLM可以輸入的內容長度

下方是byte-based tokenization的toy implementation

class ByteTokenizer:
    """Represent a string as a sequence of bytes."""
    def encode(self, string: str) -> list[int]:
        string_bytes = string.encode("utf-8")  # @inspect string_bytes
        indices = list(map(int, string_bytes))  # @inspect indices
        return indices
    def decode(self, indices: list[int]) -> str:
        string_bytes = bytes(indices)  # @inspect string_bytes
        string = string_bytes.decode("utf-8")  # @inspect string
        return string

Word-based tokenization

Word-based tokenization 是許多傳統NLP方法採用的做法,也是最符合直覺的一種方式:把文字序列拆分成單字。然而,這個方法依舊有幾個缺點

  • vocabulary size會很大
  • 會有很多根本不常出現的單字
  • vocabulary size幾乎無法固定,因為語言中的用詞是一直在變化的,而且隨時可能會有一些新詞或特殊的專有名詞
  • 如果遇到未知單字時,只能使用 來表示,這會導致許多麻煩,例如影響model計算perplexity的準確性

Byte Pair Encoding (BPE)

BPE最初是由Philip Gage在1994作為data compression的algorithm[article]被提出的,而在2015的時候被

Neural Machine Translation of Rare Words with Subword Units

這篇論文應用在neural machine translation上(在這之前的paper都是使用word-based tokenization)

  • Basic idea:直接在raw text上訓練tokenizer來決定vocabulary (詞彙表)
  • Intuition:常見的characters sequences用一個token表示, 較少出現的characters sequences用多個tokens來表示
  • GPT-2 的論文先使用word-based tokenization的方式 (透過Regular expression) 把 raw text 分割為小片段,接著在每個分割片段上執行BPE algorithm
  • Sketch:一開始會先將 raw string 轉換為 byte sequence,此時每一個 byte 就是一個初始的 token(vocabulary size 為 256)。接著演算法會不斷尋找、統計並合併最常出現的相鄰 tokens 成為一個全新的 token(例如將詞彙表擴充加入第 256、257 號),藉由這個循環來縮短序列長度並提升壓縮率。換句話說,BPE 的訓練過程就是在根據資料的統計特徵,自動建構出一張最有效率的 vocabulary。

想要理解tokenizer的運作,可以玩玩這個網頁 Tiktokenizer

一些值得注意的地方:

  • 一個單字和前面的空格屬於同個token
  • 一個同時出現在開頭和中間的單字通常屬於不同token (開頭沒有包含空格,但中間的有)
  • 數字會被切分成幾位數一組的 token,這種切分方式為由左至右,而且完全不具備數學語義的

Reading Later

筆者會將課程中提及且之後可能會參考的資料放在這裡

筆者今日回顧與碎碎念

這是筆者第一次以這樣的方式來記錄自己的學習過程,如果有哪裡寫得不好或不清楚的地方歡迎指教。老實說,從頭開始寫一篇這樣的記錄所耗費的時間遠遠超乎筆者一開始的預期(今日看影片與寫筆記/學習記錄的時間比約為 1.5:4.5),但願能一路堅持到把課程順利完成。

目前筆者的學習流程大概是 看影片 → 根據教材內容寫筆記 → 把影片丟給 NotebookLM ,針對做筆記時出現的疑問做問答 → 最後把筆記內容丟給 NotebookLM 跟影片內容比對,找出還可以補充的點。未來可能還會再摸索看看是否有其他能提升效率的方法。

今日筆者最大的收穫之一大概是找回了一些自信心,在做筆記的過程中,筆者分心的時間加起來不超過 15 分鐘,這是筆者久違地體驗了一次長時間全神貫注的狀態,也讓筆者發現自己專注在高品質輸出的狀態下,是最不容易分心的時候。

最後,以今日內容來說,筆者認為收穫最大的部分是 Efficiency drives design decisions ,這種嘗試將有限資源做效益最大化的思考方式在很多地方應該都適用,尤其是 Scaling Law 的部份,在現代各種model越train越大的情況下,可以推廣到許多應用場景中 (推薦閱讀:https://axk51013.medium.com/llm專欄-迎接2024年-10個必須要搞懂的llm概念-1-scaling-law-5f6a409d35c5)

題外話:今天剛好是筆者的生日,上週五下定決心這週一開始挑戰的時候完全沒想到這件事,今天看到有人祝筆者生日時才想起來,只能說現實中總會有些神奇的巧合,剛好在25歲生日這天開始了筆者認為設定給自己最有意義的挑戰之一。

3 Likes