克羅埃西亞之旅 – 3

2026/4/16 (四)

這天同樣搭電動車到達一號碼頭,然後坐船到三號碼頭的下湖。

下湖的水量比上湖還要大。雖然也有安靜的小角落,但常常可以看到河水漫過木棧道的景象。

從出口附近的觀景台拍的。

從導遊介紹的私房景點拍的上帝視角。

很美麗吧!不過走到這裡也是要有一點腳力。而且國家公園裡面沒有廁所,一天要走完上下湖也不是不行,但是膀胱可能不太允許。公園的配套措施是:第二天要用的門票就沿用第一天買的,給當地的旅館蓋章認證後,第二天就不用再買票。

出了國家公園,我們驅車南下,在 Restaurant Borje 用午餐。甜點是當地名產 Fritule。它跟台灣的炸雙胞胎吃起來一個味道。導遊介紹之後大家都有點嫌棄它太油什麼的。不過既然是土特產,一定要捧場啊!

車子繼續南下,我們來到很有名,有點厲害,但是又有點不好說的著名景點。札達爾 (Zadar) 的海風琴 (Morske orgulje) 和向太陽致敬 (POZDRAV SUNCU ZADAR CROATIA)。

海風琴是把管子插進海水哩,利用此處外島很多,潮水相對起伏較小的特性,讓海浪稍稍衝擊這些管子,發出高高低低的聲音,卻又不會破音。"向太陽致敬" 是將太陽系的星球做成不同大小的太陽能板,白天儲電,晚上可以發光。

比起早上的風景,這邊有點小兒科。後面的市區古建築介紹相對有趣些。Zadar 是一座濱海的羅馬古城。後來因為威尼斯人入侵 (下圖左上城門的飛獅標記),羅馬遺跡都被當石材處理 (下圖左下的教堂地基),形狀完整的東西所剩不多 ,考古學家拼不回來,就把它們直接拿來陳列 (下圖右下)。

看完古蹟之後,我們入住 Falkensteiner Hotel & Spa Ladera。裡面很高級,晚餐可以點菜又可以吃自助 Bar。當然就不老實又不客氣~~~

斯洛維尼亞 + 克羅埃西亞之旅 – 1

2026/4/14 (二)

這是一個幾乎每天換旅館的行程,早上從布雷德湖畔的 Hotel Park,經過盧比安納 – 也就是 Google Map 上的盧比爾雅那 (Ljubijana),來看一個石灰岩洞 Postojna Cave。

行前氣象預報說每天幾乎都會下雨,而這個岩洞又是最冷的一個點,洞內只有 8~9 度。所以我們還去 “迪卡儂" 買了防水衣、防水褲、防水鞋、手套、雨衣、雨傘等配備。不過運氣不錯的是沒下什麼雨,也沒那麼冷。

雖然交通順暢,我們提早到了,還是要等預定的時間 10:45 才能進場。進去洞穴要先搭小 “火" 車,然後下車徒步。這個洞穴據說是斯國第二大、但是第一個被發現的石灰岩溶洞,也是喀斯特地形被命名由來。光是坐小火車進去,沿途開錄影就錄了 7’17″,基本上這裡大到匪夷所思。逛了 50 分鐘來到車站,然後搭反向的小火車出去。

洞內據說有很多生物,但是都怕光躲起來了。其中,最特別的是盲螈,園區特別養了一些放在大型玻璃箱裡,牠們還順利生出下一代。因為不能拍照和錄影,所以只能拍官方的即時監控螢幕。紀念品店裡賣很多盲螈娃娃,基本上整隻白白不太討喜。

中餐就在岩洞旁的餐廳解決,接下來就驅車前往克羅埃西亞 (Republika Hrvatska)。因為兩國同是申根區,所以沒有邊境檢查。

抵達歐帕提亞 (Opatija) 後,導遊先帶我們去上廁所。為什麼這件事很重要呢?首先我們拉車時間比較長了一點。但更重要的是,離開斯洛維尼亞之後, “廁所自由" 就沒有了,公廁要錢,而且不是到處都有。三十個人撐著傘、大排長龍地上完廁所;大家就冒雨去海邊看 “少女與海鷗" 的銅像。

再來就沒有行程了,讓我們在度假中心好好玩。Check in 高級的 Hilton Rijeka Contabella Beach Resort 後,其實我也不想去游泳池、健身房,就去海邊探險。亞得里亞海岸的海水清澈見底。我一隻魚也沒看見,但是確實有人在釣魚。

晚餐也吃得很好。基本上我都在吃甜點。

古風 Manifold 小筆記

文天祥的正氣歌 [1] 說:"天地有正氣,雜然賦流形。" 這個 “流形" 就是 Manifold 的中文翻譯。後兩句 “下則為河嶽" 就是說一般人站在地球上,還以為地球是平的,"上則為日星" 就是抬頭一看,感到世界大到甚至不只是 3D。

正經地說,流形是一個局部看起來像歐幾里得空間(平坦的空間),但整體結構可能非常複雜且彎曲的拓樸空間。一張 1024×1024 的彩色照片,在數學上是一個高達 300 萬維度的空間,但「有意義的貓狗照片」只佔據這個 300 萬維空間裡一個極小的低維流形,不是每個空間中的值都有特殊意義。這種群聚就像是太陽系裡面的星球彼此靠得很"近",若是出了太陽系,宇宙就顯得很稀疏,直到遇到其他星系。

對 AI 來說,世界模型充滿雜訊且計算量太大。模型會透過編碼器(Encoder),將高維的觀測數據「壓縮」並映射到一個低維度的潛在空間(Latent Space) 中。這個潛在空間本質上就是在捕捉數據所在的流形。一旦 AI 掌握了這個低維流形的潛在空間,它就能在這個平滑、連續的流形上做計算。這也可以解釋 transformer – encoder、decoder 為何會有用。

當初 DeepSeek 出來的時候,也用上了 latent space 壓縮的技術。我當時想,做完這個 lossy 的壓縮,雖然節省計算量,但是資訊丟掉就回不來了啊!? 因為那個 monent 我還沒有流形的觀念;如果有,我就不用緊張了。

在數學上,流形不一定可以微分。但是要訓練 AI ,就要計算梯度。因此我們只能假設它可以被微分,也就是要假裝它更平坦一點。因此就算真實的流形不可微,我們也得製造出一個可微分的流形。所謂 “人心惟危,道心惟微" [2],人類明明知道會有危險的奇點 (Singularity),但是演算法只能讓它可微分。以目前 AI 強大的戰力來看,這個有意的忽略,似乎影響不大。

[REF]

  1. 正氣歌
  2. 十六字心傳

PPO 小註解

Proximal Policy Optimization 是一個 fine-tune Model 的方式, 另一個主流方式是 DPO (Direct Preference Optimization). 這邊主要是整理課程影片中 PPO 的公式 (只用大約 2 分鐘超高速閃過…), 以便有空時可以查詢.

[基本架構]

有一個 LLM model, 和可調整的參數: θ, query: X, response: Y, 所有輸出入組合 rollout 表示為 (X, Y).

因為我們要微調這個 model, 所以要給一個獎勵函數 (reward function): r(X,Y), 用來調整模型 πθ (X,Y).

獎勵是對所有輸出的 Y 取期望值

EY~πθ (X,Y) [r(X,Y)], 其中 Y~πθ (X,Y), 表示 Y 在 πθ (X,Y) 的輸出組合.

也對每個 query X 計算

EX~D[EY~πθ (X,Y) [r(X,Y)]] ——————— (1)

其中 X~D, D 表示 X 的散度 (Divergence), 依此類推.

我們找出一個得到最多獎勵的那組參數, 就算是 fine-tune 完成.

π* (X,Y) = arg maxπ {EX~D[EY~πθ (X,Y) [r(X,Y)]]}, {} 裡面就是上面那包.

[Reference model]

做得好不好, 我們要有一個 reference model 來當評審.

πref (X,Y)

Model 的輸出愈接近 reference model 就表示學得愈好.

[懲罰值]

KL 是 Kullback-Leibler 兩個人名的縮寫. KL penality coefficient 是它們衡量懲罰的函數. 基於 reference model πref (X,Y) 下, 對 πθ (X,Y) 產生的懲罰 DKL 表示為

DKLθ (X,Y) || πref (X,Y)]

[綜合期望值和懲罰值]

π* (X,Y) = arg maxπ {EX~D[EY~πθ (X,Y) [r(X,Y)]] – β * 懲罰值} =>

π* (X,Y) = arg maxπ {EX~D[EY~πθ (X,Y) [r(X,Y)]] – β DKLθ (X,Y) || πref (X,Y)]}


為了要知道往哪個方向調整, 我們要找 θ 的梯度.

[只看 reward function]

這是 reward function 在特定 model 參數 θ 的期望值

E[rY ] = EY~πθ (X,Y) [r(X,Y)]

將它表示為解析函數 [1], 期望值等於所有 Y 可能的值乘上它的機率的和

E[rY ] = ∑Y r(X,Y)πθ (Y|X) —— (2)

最優化的參數 θ⌃ 就是所有 θ 中 reward 最好的. (我打不出 hat theta, 所以把帽子放右邊)

θ⌃ = arg maxθ [E[rY ]] = arg maxθ [∑Y r(X,Y)πθ (Y|X)]

對式子 (2) 取 θ 的梯度,

θ E[rY ] = ∑Y r(X,Y)θ πθ (Y|X)] ——– (3)

按照微積分的 chain rule [3]

θ log (πθ (Y|X)) = θπθ (Y|X) / πθ (Y|X) =>

θπθ (Y|X) = θ log (πθ (Y|X)) * πθ (Y|X) —– (4)

把這個變長的 (4) 帶回去 (3),

θ E[rY ] = ∑Y r(X,Y)θ πθ (Y|X)] =>

θ E[rY ] = ∑Y r(X,Y)θ log (πθ (Y|X)) * πθ (Y|X) =>

θ E[rY ] = EY~πθ (Y|X) r(X,Y)θ log (πθ (Y|X))

再把 X 考慮進去, 就得到

Ex~D [θ E[rY ] ] = θ Ex~D [ E[rY ] ]

意思是說, 當 β = 0, 忽略掉懲罰的話, 對式子 (1) 取梯度, 等效只對 reward function 取梯度. 反之亦然.

這有什麼意義呢? 我請 AI 幫我解釋. 我試了幾次都沒有它講得好.

θ E[rY ] = EY~πθ (Y|X) r(X,Y)θ log (πθ (Y|X)) 這個結果是 策略梯度定理(Policy Gradient Theorem)[2] 的核心公式。其意義在於:

  • 將梯度轉換為期望形式:原始梯度需遍歷所有可能的輸出 θY(計算量巨大),但此公式表明:梯度可通過從策略 πθ   中採樣 Y 來近似計算。
  • 避開解析計算:無需知道所有 Y 的機率分佈,只需對採樣的 Y 計算  r(X,Y)θ log (πθ (Y|X)) 的平均值。

它與 Monte Carlo Method 的相似處在於: 透過採樣直接估計期望值,無需精確計算所有可能的 Y.

蒙特卡羅方法收斂速度為 O(1/√N​), N 夠大就好. 而窮舉法不可行.

最後再筆記一下懲罰項.

Kullback-Leibler 方法其實是要避免訓練後新的參數和舊的參數偏差太遠. 以至於學會新的東西就忘了舊的 – 災難性遺忘 (Catastrophic Forgetting), 和 reward 無關. 因此討論原理時可以忽略它.

但它對於穩定系統非常重要, 其主要角色是作為正則化項 (Regularizer), 將策略更新限制在信賴域 (Trust Region) 內. 在PPO算法中,KL 散度還會動態調整更新步長. 當 DKL​ 超過閾值時自動縮小學習率,此設計本質上將 “避免遺忘" 和 “促進學習" 綁定為同一過程。

[REF]

  1. 解析函數 – 維基百科,自由的百科全書
  2. https://en.wikipedia.org/wiki/Policy_gradient_method
  3. Slide 1

GGUF 小註解

拜台幣升值 10% 左右之賜, 我在 Q2 的投資差點要 GG. 我期望不高, 只要月底能跟 Q1 對齊, 也算是有 10% 看不見的成長了. 在這混亂的國際局勢下, 我來筆記一下 GGUF. 它不是 “GG, U Failed" 的縮寫! 而是全名 GPT-Generated Unified Format, 一個為了 LLM 推理而設計出的檔案格式.

一般我們去 Huggingface 下載一個模型, 它會可能是 .bin 檔, safetensor 檔, 或是 ONNX 檔. 以 safetensor 而言, 裡面放的是模型未量化過的模型權重. 至於 model 的架構 (graph 長相), 會另外寫在 config.json 裡面. 雖然這個檔案名稱也通用於很多其他的地方, 這裡特別是指用來描述模型的 config.json.

例如 Gemma-2-2B 的 config.json [1] :

{
  "architectures": [
    "Gemma2ForCausalLM"
  ],
  "attention_bias": false,
  "attention_dropout": 0.0,
  "attn_logit_softcapping": 50.0,
  "bos_token_id": 2,
  "cache_implementation": "hybrid",
  "eos_token_id": 1,
  "final_logit_softcapping": 30.0,
  "head_dim": 256,
  "hidden_act": "gelu_pytorch_tanh",
  "hidden_activation": "gelu_pytorch_tanh",
  "hidden_size": 2304,
  "initializer_range": 0.02,
  "intermediate_size": 9216,
  "max_position_embeddings": 8192,
  "model_type": "gemma2",
  "num_attention_heads": 8,
  "num_hidden_layers": 26,
  "num_key_value_heads": 4,
  "pad_token_id": 0,
  "query_pre_attn_scalar": 256,
  "rms_norm_eps": 1e-06,
  "rope_theta": 10000.0,
  "sliding_window": 4096,
  "torch_dtype": "float32",
  "transformers_version": "4.42.4",
  "use_cache": true,
  "vocab_size": 256000
}

使用 GGUF 就不用單獨的 config.json 了, 但是生成 GGUF 的時候還是需要. 所以從Huggingface 下載 GGUF 時看到 config.json 也不用太驚訝. 總之, GGUF 檔案裡面除了放模型的權重, 還放了超參數和詞彙表. 所謂超參數就是模型訓練中需要手動設定、無法透過模型訓練的參數 – 這些就是從 config.json 抄過來的.

至於詞彙表 (Vacabulary) 裡面就是放所有 toekn 的字典. 它有一點大, 但不會太大. 假設u有個模型認得 128K 個 token (大約是 GPT-4 的用量), 每個 token 長度 4 bytes, 算起來才 5.1MB, 比起模型權重動輒就是幾 GB, 真的也不差多哪一點.

GGUF 把這些都打包起來, 使用時就不用東市買駿馬,西市買鞍韉,南市買轡頭,北市買長鞭. 假設要量化參數, 下個指令轉出新的 .gguf 檔就好. 包括混合精度也能做到. 更不會改了參數檔, 結果 config.json 沒改到. 我們常常會遇到這個 bug.

雖然 GGUF 把相關資料都包在一起, 但它執行起來並不像個肥宅, 甚至它更省記憶體. 如果我們使用 safetensor, 因為那一整包都是權重, 我們無法知道哪一個 byte 是屬於哪一層的參數, 所以通常整包都得放進記憶體. 像是 Llama 3 70B 量化過也需要用 30GB 記憶體.

反觀 GGUF 自己就有完整的訊息, 它可以把檔案中的模型 memory mapping 到 virtual memory 裡面, run 到哪個 chunk, CPU 或 GPU 直接看 index 載入哪部分的參數即可, 號稱可以作到 zero copy. 以前述 Llama 3 70B 模型為例, 可能只需要 500MB~2GB 的記憶體就夠了. 據說在一台 16GB 的筆電上也能運行.

另外, safetensor 檔案裡面只放權重, 還大剌剌地以 safe 為名. GGUF 包了更多東西進去, 那還能保證安全嗎? 答案也是肯定的. 它對格式和 API 都有所規範, 並內建 SHA-256 校驗, 所以不至於輕易地被駭客埋 code 進去, 並且被當作 Python 執行. 至於 pickle (.pt) 或是 .bin 檔案則是有這方面風險.

最後談 GGUF 的缺點. 首先是它只支援 LLM, 不像 ONNX 適用於所有深度學習 (Yolo 等等)、safetensor 更適用於儲存所有 tensor. 其次是背後沒有大人認養, 所以 toolchain 比較不足. 最後的主要缺點是 – 它對 CPU 優化較好, 同一個 model 用 ONNX 表示的話, 在 GPU 上會跑得更快.

最後叫 Monica 生成一個簡單的 GGUF 檔案範本, 這樣比較好理解.

[檔案頭 Metadata]
general.name = "MiniModel-v1"
general.description = "示範用微型語言模型"
general.file_type = 1 (FP32)
general.architecture = "Transformer"
general.quantization_version = 2

[tensors]
1. token_embedding.weight
   - shape: [5, 3]  # 5個token,每個token向量大小為3
   - data: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], ...]

2. layer.0.attention.wq.weight
   - shape: [3, 2]  # 3維輸入 → 2維輸出
   - data: [[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]]

3. layer.0.ffn.w1.weight
   - shape: [2, 4]
   - data: [[...], ...]

實際上 head -c 1000 granite-7b-lab-Q4_K_M.gguf | xxd -c 32, 可以看到檔案前面有幾個文字可讀, 後面 tensor 就都是 binary. 如果仔細看下圖, 會看到裡面還看到 ggml 的字樣, 那是 GGUF 的前代格式. 主要是差在舊版沒有 chunked index 結構, 不方便快速抓出某一層參數, 以及沒有 SHA 保護機制.

[REF]

  1. https://blog.csdn.net/shizheng_Li/article/details/144866526