一言以蔽之:ChatGPT 其實就是文字接龍而已。
好啊,我們來開始上課吧。
今天這句話我要放在最前面講,因為它就是整堂課的核心:ChatGPT 這麼神奇,它到底在幹嘛?它其實就是在玩文字接龍而已。
你現在大概覺得我在唬爛。一個可以幫你寫程式、幫你改論文、跟你討論人生的東西,你跟我說它只是在接字?
對。今天這堂課我們就要來證明這句話是真的。而且不是用嘴巴證明——我們要在這個網頁裡,真的 train 一個語言模型出來。沒有預錄、沒有假資料,下面每一段中文都是你的瀏覽器現場算出來的。
這堂課我們分成六段:
最後我們會回答一個問題:它為什麼會胡說八道?
這一版是互動的。每一個滑桿你都可以拉、每一個按鈕你都可以按,文字會馬上重新生成。請務必動手——這堂課有一大半的東西是動手才會有感覺的。
你每天都在用這種聊天機器人。所以我不從「什麼是語言模型」開始講,那個太無聊了。我從你已經有感覺的兩件事開始。
現象一:它為什麼可以一直講下去?你問它一句話,它可以講一大段;你叫它繼續,它還能繼續。它好像永遠不會「沒話講」。這件事本身就很奇怪對不對?
現象二:它為什麼會一本正經地胡說八道?你問一個很冷門的問題,它給你一個聽起來超級專業、有頭有尾、連參考文獻都幫你列好的答案。結果你去查,那篇論文不存在。
欸這就很妙了。它不是「我不知道」,它是很有自信地講了一個假的東西。
你可能會想說:這兩件事應該沒什麼關係吧?一個是優點,一個是缺點。
它們是同一件事。今天講完你就會知道為什麼。
好,那我們從最基本的地方開始。假設現在螢幕上已經有一段文字:早上小明一個人在教。請問下一個字是什麼?
你腦中大概會冒出「室」。你怎麼知道的?因為你看過很多中文,你知道「在教」後面接「室」很合理,接「鳳梨」很怪。
好,這就是全部了。語言模型在做的事情就是這件事:輸入是目前為止的所有文字,輸出是下一個字是什麼。產生一整段文字,就是把這件事重複做很多次——接一個字 → 把它加到後面 → 再拿去問下一個字 → 再接一個字 → ⋯⋯
這就是「文字接龍」。現象一解決了:它為什麼可以一直講下去?因為它每次只負責接一個字啊。只要有前文,它就永遠有得接。它根本沒有「講完」這個概念。
這邊有一個轉折,你要看清楚。
「下一個字是什麼」——所有可能的字有幾個?假設中文常用字有五千個,那答案就是從這五千個裡面挑一個。從固定的幾個選項裡面挑一個,這叫什麼?這叫分類(classification)。
所以語言模型不是什麼神秘的東西。它就是一個五千選一的分類器而已,只是這個分類題它一秒鐘要做好幾百次。
那我們把第 1 講的三步驟套進去看看:
你看,還是同一件事。第 1 講講的三步驟,到今天完全沒有變,變的只有第一步的 model 長什麼樣子。我第 1 講就跟你講過了對不對。
做機器學習永遠的第一步:先看資料。我們今天不下載任何東西,訓練文字是用五個句型 + 五組可替換的詞,在你的瀏覽器裡現場生出來的。
為什麼用句型而不是隨便抓一篇文章?因為我們的模型很小、資料很少。用有規律的文字,你才看得出來它到底有沒有學到東西。等一下你就知道我的用意了。
然後電腦不認識「早」這個字,它只認識數字,所以第一件事是編號。這裡我們做的是 character-level(字元級)——一個字就是一個單位。真正的大模型用的是介於「字」跟「詞」中間的東西,叫做 token(可以想成詞的碎片),但道理完全一樣:把文字切成一個一個單位,然後編號。就這樣而已。
好,資料準備好了。接下來我們來做一件很笨的事。
假設你完全不會機器學習,但我叫你做文字接龍。你會怎麼做?
你八成會說:這簡單啊,我去數嘛。我把整篇文章拿來,數「早」這個字後面接過哪些字、各接了幾次。下次看到「早」,我就照這個比例抽一個出來。
這個想法非常合理,而且它有名字,叫做 bigram(二元組)。「bi」就是二,意思是一次只看兩個字:前一個字,跟後一個字。所謂的 bigram model,其實就是一張表而已:
分子是「i 後面接過幾次 j」,分母是「i 後面總共接過幾個字」。數數字然後除一除,沒有別的了。下面那張表就是它本人——點任何一個字,看它後面都接了什麼。
先講公道話:它不是完全不會。loss 從亂猜的 … 掉到 …,而且它寫出來的東西看起來是有中文字的形狀的,詞也大致是對的。
但你仔細讀上面那段。找找看有沒有這幾種東西:「助教」後面接「教室」,黏成「助教室」;開頭是「如果」,結尾卻是問號;一句話講到一半換了句型,前半句還在「小明問老師:」,後半句已經滑到「⋯⋯可是老師不同意。」去了。按幾次「換一段文章」,錯的地方每次都不一樣,但毛病永遠是同一種。看出問題了嗎?
它每接一個字都接得很順,但整句話是壞的。
它從一個句型滑到另一個句型,自己完全不知道。
為什麼呢?因為它每接一個字,就把前面全部忘光了。它看到「教」的時候,它不知道這個「教」是「助教」的教,還是「教室」的教。它只看得到一個字,它沒有記憶。
這就是 bigram 的天花板。那怎麼辦呢?
這個想法太自然了。既然看一個字不夠,那我看前面 N 個字嘛。看前 2 個字叫 trigram,看前 N 個字就叫 n-gram。作法完全一樣,只是那張表從「一個字 → 下一個字」,變成「前 N 個字的組合 → 下一個字」。
聽起來完全沒問題對不對?那我們來算一下這張表要多大。字表有 個字,看前 個字,前文的組合有 種。我們的 是 …。你自己心裡先猜一下, 的時候是多少。猜完再拉下面的滑桿。
你把滑桿拉到 8 看看。前文有 … 種組合,而我們手上只有 … 個字。你有八百兆個格子要填,但你只有五千筆資料。就算你把全世界的中文都拿來,也填不滿。
而且那個讀數還有兩欄要看。第一欄是「真的出現過的前文」: 的時候是 …。你把這個數字跟八百兆放在一起看——你有一張八百兆格的表,總共只填到了兩千多格,其他全是空的。空格代表什麼?代表你遇到沒看過的前文時,連猜都沒得猜——分母是 0,這個模型直接當機。
第二欄更狠: 越大,「只出現過一次」的前文比例越高。到了 ,… 的前文在整份資料裡只出現過一次。只出現過一次你要怎麼估機率?你只能說「這個前文後面 100% 接這個字」,然後就徹底 overfit 了。
而且注意喔,我們這份資料是用五個句型生出來的,重複性高到不像話。換成真實世界的文章,這兩個數字只會更絕望。
這個問題叫做 sparsity(稀疏)。白話文就是:格子太多,資料太少,大部分格子永遠是空的。
而且這不是「資料再多一點就好」的問題。 是指數成長,你資料加十倍,我 N 加 1 就吃掉了。這條路是死的。
那怎麼辦呢?
好,那神奇的地方來了。
我們卡住的原因是什麼?是因為我們一直想把答案存起來。存成一張表,每個前文對應一個答案。表太大了,所以不行。
那換個想法:我們不要存答案,我們去學一個函式。
這句話有沒有很耳熟?對,這就是第 1 講的第一句話:機器學習就是找一個函式。
函式的好處是什麼?函式不用把每個輸入都存起來。你給它一個沒看過的輸入,它一樣算得出一個輸出。比如說我們的資料裡有「在教室寫程式」也有「在宿舍寫程式」,表格法會把這兩個當成完全無關的兩格;但函式可以學到「教室跟宿舍是同一類東西」,於是即使它沒看過「在頂樓寫程式」,它也猜得出來後面該接什麼。
這就是為什麼要用神經網路。不是因為它比較潮,是因為表格法真的走不下去了。
先把架構講清楚,四層而已:
我們固定只看前 8 個字。這個 8 有個名字,叫 context window(上下文長度)。
你可能會想說:那不是跟 n-gram 一樣只看 8 個字嗎?對,看的範圍一樣,但存法完全不同——n-gram 要存 … 格,我們這個模型只有 … 個參數。表格法要存的東西是我們模型參數的 … 倍。差別就在這裡。
這是今天第一個要拆光環的詞。Embedding 聽起來很玄,它其實就是一張表。
我們有 … 個字,我準備一張 … 列的表,每一列是一個長度 … 的向量。看到第 5 個字,我就把第 5 列抓出來。就結束了。
那這張表裡的數字是誰決定的?沒有人決定,是 train 出來的。這張表就是模型參數的一部分,跟 、 一樣會被 gradient descent 更新。我們沒有告訴機器任何一個字的意思,我們只叫它做文字接龍——為了把接龍做好,它自己會去調整這張表。
那為什麼要多這一層?直接用編號當輸入不行嗎?不行。因為編號是我隨便排的,編號 5 跟編號 6 的字一點關係都沒有。你直接把編號丟進去,等於在告訴模型「5 號字比 4 號字大一點」,那是完全沒有意義的資訊。換成向量以後,字跟字的關係就變成可以學的東西了。
模型最後吐出 … 個機率,正確答案是其中一個字。那要怎麼打分數?第 1 講我們用 MSE,這裡我們要換成 cross-entropy(交叉熵):
就這樣。模型給正確答案的機率,取 log,加負號。沒有更複雜的東西。
那你一定會問:為什麼不能用 MSE?假設正確答案是「室」,模型 A 給它 0.01 的機率,模型 B 給 0.001——B 錯得離譜十倍,該不該被罵得更慘?當然要吧。我們來看兩種 loss 各自怎麼罰:
你會發現 MSE 到後面就罵不動了。機率從 0.01 掉到 0.001,MSE 的懲罰從 0.980 變成 0.998,幾乎沒差,它已經懶得罵你了。懲罰沒變代表什麼?代表梯度接近 0;梯度接近 0 代表什麼?第 1 講講過——參數不會動。模型錯得離譜卻收到一個「還好啦」的訊號,它就卡在那邊 train 不動了。
cross-entropy 不會。你越是打死不信正確答案,它就罵你罵得越兇。分類問題用 cross-entropy,理由就是這個。
還有一個地方順便講掉:softmax 配 cross-entropy 的時候,梯度會化簡成一個漂亮到不像話的形式:
「模型的機率」減掉「正確答案」。就這樣。意思很直白:你給錯的字多少機率,就往下壓多少;正確答案你給了 ,那就往上推 。你越有自信答錯,被推得越用力。
下面這個東西你一定要按。它是真的在你的瀏覽器裡跑 forward、backward、Adam,沒有預錄的影片,也沒有事先算好的答案。
而且我們會在四個時間點各叫它寫一段文字:還沒 train 的時候、第 … 步、第 … 步、train 完。四段用的是同一組隨機種子、同一個生成函式,差別只在參數被 train 過了多少。
從一團亂碼,變成通順的句子。而且它學到的不只是「哪些字常一起出現」:
而我們沒有教它任何一條規則。我們只叫它做一件事:把下一個字接對。資料倒進去,硬 train 一發,這些東西它自己就長出來了。
你看它生成出來的東西:它會自己在該結束的地方畫句號,然後換行。我們從來沒有寫過任何一行「如果句子夠長就停下來」的程式。那它是怎麼知道的?
答案很反高潮:「結束」對它來說,也只是一個字而已。
句號是一個字,換行也是一個字,它們跟「教」「室」在模型眼中沒有任何差別,都只是字表裡的一個編號。所以「什麼時候該結束」根本不用特別教——它在學文字接龍的時候就順便學會了。證據在這裡(這是剛剛 train 完的模型現場算的):
三步接得非常果斷。真正的大模型也是這樣做的,只是它們會另外準備一個特別的 token 叫 EOS(end of sequence)。模型接出 EOS 的那一刻,程式就知道「好,它講完了」,迴圈就停下來。所以「它什麼時候停」的答案是:它想停的時候就停,而「想停」也是 train 出來的。
不然就變成賣藥的了。我另外用同一組句型生了一份模型沒看過的文字,拿去量它的 loss:
你會發現一件很有意思的事:訓練 loss 比理論下限還低。
「理論下限」是什麼?我生資料的時候,每句話都是隨機選一個句型再填五個空格,這六個選擇是我用亂數決定的,神仙也猜不到。所以就算是一個完美的模型,loss 也不可能低於那個數字。
那我們怎麼會低於它?除非——它在背答案。
對,我們的模型 overfit 了。一萬多個參數配五千個字,它有足夠的容量把訓練資料整段背起來。所以你換一份它沒看過的文字,loss 立刻彈高一大截。它背得很熟,但沒有你以為的那麼會舉一反三。
這就是第 3 講講的 overfitting,在這裡活生生出現一次。我為什麼要特別講這個?因為這件事本身就是 hallucination 的伏筆。等一下你就知道了。
好,接下來這一段最實用,你今天回去馬上用得到。
我們的模型現在會吐出 … 個機率。問題來了:拿到機率之後,到底要怎麼挑那個字?
最直覺的作法:挑機率最高的那個啊。這個叫 greedy decoding(貪婪解碼)。聽起來完全沒毛病對不對?每一步都選最好的,結果一定最好嘛。我們來跑跑看:
你看,它寫著寫著就開始一字不差地重複前面寫過的句子,然後永遠出不來。
為什麼呢?想想看,greedy 是完全確定性的——同樣的前 8 個字,一定得到同樣的下一個字。所以只要前 8 個字曾經重複出現過一次,後面就一定完全重複。它掉進去就出不來了。
這不是我們模型的 bug 喔。這是 greedy decoding 本身的性質。你用最大的模型,開 greedy,一樣會鬼打牆。
而且 greedy 還有第二個問題:它每次都挑最有把握的字,所以寫出來的東西一定是最平庸、最無聊的那種。你叫它寫詩,它會寫出全世界最安全的詩。
那怎麼辦呢?很簡單嘛——不要每次都挑第一名,照機率抽就好了。機率 0.5 的字就有一半機會被抽到,機率 0.01 的字就偶爾出現一次。這叫 sampling(取樣)。
但這樣又有新問題:抽太保守,還是很無聊;抽太放,就開始亂講。我要一個旋鈕,可以調鬆緊。這個旋鈕就叫 temperature(溫度)。
temperature 聽起來很厲害,它其實就是在 softmax 之前先除一個數字而已:
就這樣。一個除法,然後就結束了。那它為什麼有用? 很小,所有分數的差距被拉開,最高分那個會被推到接近 1 → 幾乎等於 greedy; 很大,差距被壓扁,大家機率都差不多 → 越來越接近亂抽。
與其用講的,你自己拉。左邊那張圖是 log 尺度——請盯著那條「尾巴」看。
把溫度慢慢往上推,看那條尾巴怎麼被整條抬起來。抬多少?右邊那張圖才是重點——「第 5 名以後的字」加起來佔的機率,從幾乎是 0,一路漲到三成以上。
你想想看這代表什麼:溫度開到最高的時候,有將近三分之一的機率是分給那些「根本不該被選到」的字。也就是說你每接三個字,大概就有一個是從垃圾堆裡抽出來的。
而那個錯字接上去以後,它又變成後面的前文,整句話就跟著歪掉了。這就是為什麼高溫生成看起來會「發瘋」——不是因為它每個字都亂寫,是因為它偶爾寫一個超級離譜的字,然後錯誤會自己滾雪球。
那怎麼辦呢?
很粗暴但很有效的作法:只留機率最高的 k 個字,其他直接不准選。這就是 top-k sampling。
這樣你就可以「又要溫度高、又不要它亂講」——溫度負責在合理的候選字裡面增加變化,top-k 負責把不合理的選項擋在門外。
下面兩根滑桿一起玩:先把溫度拉到 2.5 看它發瘋,然後把 k 從 73 一路往下拉,看文字怎麼一句一句長回來。
你把 k 拉到 1 看看——那就是 greedy,鬼打牆又回來了。往上拉一點點,句子開始有變化;拉太高,錯字就跟著回來。這中間有一個甜蜜點,而它會隨著溫度移動。這就是為什麼實務上這兩個旋鈕要一起調。
你把上面所有的輸出擺在一起看:greedy 的、低溫的、高溫的、加了 top-k 的。它們看起來像是四個不同的模型寫的對不對?
可是它們是同一個模型。參數一個都沒有變,一根寒毛都沒動。我們從頭到尾只改了「拿到機率之後怎麼挑」而已。
這件事給你的實務教訓是:
當你覺得一個模型「很爛」的時候,先確認一下是不是 sampling 參數沒調好。
這是真的很常發生。有人抱怨模型講話很無聊、一直重複——結果是 temperature 設太低。有人抱怨模型在胡言亂語——結果是 temperature 開太高又沒設 top-k。模型沒問題,是旋鈕沒轉對。
好,我們回到開場的現象二。
我現在給模型一個開頭,然後問它下一個字。你想想看,這個問題有標準答案嗎?
同一個問題問六次,它給了你好幾個不同的答案。而且每一次都講得斬釘截鐵,一點都不心虛。
為什麼會這樣?因為我在生訓練資料的時候,那個地點是我用亂數填的。老師半夜在哪裡,這件事在資料裡根本沒有答案。
而且模型自己知道它不知道——你看上面那張機率分布,就是在猜嘛。但是。一旦我們抽了一個字出來接上去,這個「猜」的痕跡就消失了。讀者看到的是一句斬釘截鐵的話,沒有任何地方寫著「本句信心水準 …」。
這就是 hallucination 的機制。你回頭看我們今天做的每一件事:
從頭到尾,沒有任何一項在檢查「這句話是不是真的」。
它的訓練目標從頭到尾只有「下一個字接得像不像」,沒有一項是「講得對不對」。
「像」跟「對」是兩件事。它被訓練成很會「像」,所以它會產生讀起來非常像真的、但完全是編的東西。它不是在說謊——說謊要先知道真相。它只是在接字而已。
而且還記得剛剛的 overfitting 嗎?資料裡真的有的東西,它背得很熟;資料裡沒有的東西,它就用學到的句型幫你補一個,而且補得非常通順。這就是為什麼它可以幫你「列出」一篇不存在的論文——論文標題的格式它學得很好,至於那篇論文存不存在,從來就不在它的 loss 裡面。
這裡我們撞上最後一面牆。
剛剛 train 的時候,我們一直盯著 cross-entropy,它變小我們就很開心。可是你已經親眼看到:model 可以把句子接得很順,同時把根本不知道的地點講得斬釘截鐵。
所以只拿一把尺來量生成式 AI,會發生什麼事?它會開始專門討好那把尺。
比如說,考試只看文句通不通順,model 就可能交出一篇非常順、事實全部亂寫的答案。只看答案有沒有命中某個關鍵字,它也可能學會塞關鍵字,根本沒有回答問題。
怎麼辦呢?不是發明一個號稱「萬能」的分數,而是把問題拆開:
這四把尺有時候還會互相打架。拒絕所有問題也許很安全,但完全沒用;每個問題都講得又快又滿,也許看起來很能幹,但出錯時更危險。
評估不是替 model 頒一張總成績單,而是先說清楚你在乎哪一種「好」。
這也是為什麼看到 benchmark 上的一個數字時,不要立刻問「誰比較強」。先問:它量的是哪一種能力?沒量到的是什麼?數字沒有騙你,是你可能問錯了數字。
誤解一:「模型是先在心裡想好一整句話,再講出來的。」
很多人會這樣覺得,但其實不是。它是一個字一個字接的,而且接每一個字的時候,它並不知道自己這句話最後要講什麼。你看我們的生成迴圈就知道了——裡面沒有任何「計畫」的步驟,只有:算機率、抽一個、接上去、再算一次。那為什麼講出來的話還是有頭有尾?因為「有頭有尾」這件事本身,也是從訓練資料裡學到的接字習慣而已。
誤解二:「temperature 是在調模型的創造力。」
千萬不要這樣講。temperature 一根寒毛都沒有碰到模型。你剛剛在「動手玩 ④」拉滑桿的時候,模型算出來的那 … 個分數從頭到尾一模一樣,變的只是你怎麼從那些分數裡面抽。
這個差別很重要:模型的能力是 train 出來的,改 temperature 改不了它的能力,只能改它的發揮方式。你把溫度調高,它不會因此多知道一件事實——它只會更常去抽那些原本就排在後面的字。
我們要誠實。差蠻多的。但差在哪裡,你現在有能力聽懂了。
要讓它變成「會回答你」的助理,後面通常還有兩段路:
後兩段常被合稱為對齊訓練:讓 model 的行為更接近人類真正想要的樣子。但不要誤會喔——對齊不是把文字接龍引擎拆掉換一顆新的。核心仍然是預測下一個 token,差別是它又看了新的例子、接受了新的評分方式。
核心的機制,跟你剛剛在這個頁面上 train 出來的那個,是一樣的。輸入目前為止的文字,輸出下一個字的機率分布,抽一個,接回去,再算一次。就結束了。
三題,選了馬上告訴你對不對。
光看是學不會的,你一定要自己動手。
練習 3 你會算出一個很誇張的數字。這正是為什麼需要 attention——把 context window 用「攤平接一個大矩陣」的方式做,是撐不住的。(答案:… 個參數,是現在的 125 倍。)
然後有一個觀念,我希望你今天下課只記得這一個:它的訓練目標從頭到尾只有「接得像不像」,沒有一項是「講得對不對」。這就是為什麼它會一本正經地胡說八道。這不是 bug,這是文字接龍這個目標本來就管不到的地方。
還有一個配套的觀念:評估不是頒一張總成績單。你要先說清楚在乎哪一種「好」,因為那四把尺會互相打架。
最後,我們回到開場那句話。我一開始說:ChatGPT 其實就是文字接龍而已。你那時候大概覺得我在唬爛。
現在你已經在這個頁面上 train 過一個了。你看過那個迴圈:算機率、抽一個字、接回去、再算一次。裡面沒有魔法。那句話是真的。
文字有一個很棒的性質:它有先後順序。第一個字、第二個字、第三個字。所以我們可以一個一個接,接到天亮。
那圖片呢?
一張圖有一百萬個 pixel。請問哪一個是「第一個」pixel?左上角嗎?那為什麼不是右下角?中間那個 pixel 的「下一個」是誰?
圖片沒有天生的順序。所以「一個一個接」這招,在圖片上直接失效。
那怎麼辦呢?下一堂課我們來看一個完全不同的想法——不要一個一個接,而是先把整張圖弄得亂七八糟,再學著把它一步一步還原回去。那個東西叫 Diffusion。
以上就是我今天想跟大家分享的內容。