跳到主要內容

Greatest notes compiler 知識蒸餾流程

 1. Greatest notes format

平常我就有作筆記的習慣,其中大部份的筆記都是非結構化的文字檔,部份是 markdown 格式,這樣在任何平台只要有文字編輯器,就能很方便檢視及編輯,而不必依賴特定的應用程式。

久了就慢慢累積了不少資料,有日記、工作日誌、筆記、雜記等等。為了方便檢視,我也作了一個小 app,可以讓我在一個畫面內快速檢視我的筆記列表,再點入去作進一步的檢視各別資料或統計資訊等。

雖說這些筆記幾乎是非結構化的,但其實還是有少量的規則,這樣我的程式才能作簡單的解析。例如日記或工作日誌基於日期的資料,至少有個日期標題和不定長度的內文,或是筆記類的資料至少也有個標題和不定長度的內文,等等。

漸漸的我盡量把類似性質的筆記的格式統一起來,最後我發現其實可以用一個很簡單的格式全部統一。它就是 Greatest notes 格式。

Greatest notes 文件是文字檔。內容由不定數量的項目組成,每個項目由一個標題和不定長度的內文組成。每一個項目由每一行開頭的">>> "開始,直到下一個">>> "或者是檔尾,就是一個項目。

例如:

>>> 這是一則筆記
內文
內文
內文
...

>>> 另一則筆記
內文
內文
...

格式很簡單,要寫一個解析器也很簡單。因為只需要 re 以 ">>> " 對整個檔案切割,再從每個項目的第一行取出標題,其餘為內容。

例如下面就是一段可以解析 Greatest notes 文件的 javascript 程式。

// javascript
let notes = raw.split(/^>>> /gm).map(note => {
    let i = note.indexOf('\n');
    let title = note.substring(0, i);
    let content = note.substring(i + 1);
    return {
        title: title !== '' ? title : '未命名筆記',
        content: content
    };
}).filter(note => note.content !== ''); // Remove empty notes.

因為這是一個極簡的格式,只以 ">>> " 對檔案內容作區隔。缺少巢狀結構和 metadata 等。但實際應用時,能在標題帶入某種簡單的格式作為 metadata 的額外補充。例如日記或日誌等資料,可以在標題中統一以一個日期開始等。

有了這個簡單的格式,要建立筆記就方便了。在標題加上少量的自定義 metadata 就能進一步增強文件的結構,有利於進一步整理及資料分析。

2. Greatest notes compiler

近幾年 LLM 崛起,我也和大家一樣利用 AI 輔助探索了許多感興趣或遭遇到的問題。

OpenAI ChatGPT 有個對我很方便的地方是,可以容易的把整個對話資料匯出備份。匯出的資料全部被打包成一個 zip,下載後解壓。裡面有一個對話資料有關的 json 檔案。

後來我利用 AI 輔助,寫了一個讀取並解析此檔的工具程式,可以在電腦上的 browser 檢視。之所以會另外作一個工具檢視,而不是只使用官方的 app。是因為我常常會回頭檢視歷史紀錄,而當對話很長時,檢視已經變得很不方便。我作的小工具主要是為了提供一個大綱模式,讓我可以快速跳至指定段落,方便閱讀。

因為前面我已經發展出 Greatest notes 格式和手機檢視小工具,所以很自然的,我也把 ChatGPT 的 json 格式的對話紀錄轉成 Greatest notes 格式,並匯入我的小工具,在手機上方便隨時檢閱。

但是長對話的問題還是存在。超長的文件不管用什麼方式檢視都是個問題。就算我把那些問答式的對話,或是些無關緊要或價值不大的對話刪除,轉換成 Greatest notes 後,許多對話都有幾萬字,整個文件將近一百萬字。

基於這個原因,我才發展出 Greatest notes compiler,可以把 Greatest notes 文件內容壓縮,提取摘要和重點,過濾掉不是重點的內容。最後一樣輸出另一個 Greatest notes,這樣我可以互相比對參考。

2.1 Greatest notes compiler pipeline

Greatest notes 編譯器是一個 pipeline 流水線,類似一般的程式語言的編譯和連結流程。

load_greatest_notes
   ↓                載入 Greatest notes
do_compile
   ↓                對個別 note 編譯,輸出個別 json
merge
   ↓                合併所有 json,輸出最終 Greatest notes
  完成

2.1.1 編譯流程

編譯流程首先載入 Greatest notes 後,分別對個別的 note 執行編譯。每個 note 編譯完成後會個別輸出一個 temp 的 json 檔案,類似編譯 c/c++ 程式時,會產生中間的 obj 檔案。分成幾個步驟如下:

compile_note
   ↓                對單一則 note 編譯
chunk_text
   ↓                對 note 以固定長度作切割
summarize_chunk
   ↓                對 chunk 內容作摘要提取 
extract_claims
   ↓                對 chunk 內容作要點提取
  完成

2.1.1.1 chunk_text

chunk_text 主要目的是對輸入的 note 內文作切割,避免單一 note 內文過大,造成後續編譯步驟出問題。

切割的方式很簡單很暴力,就是以固定長度對輸入文字切割。目前以 chunk_size = 2000 作區段大小處理。雖然用這麼簡單又暴力的方式切割文字,但實測結果並沒有什麼明顯的問題,所以也就暫時都這樣作了。

2.1.1.2 summarize_chunk

summarize_chunk 利用 LLM 的語言能力對輸入文字作摘要提取。目前使用的模型是 gemma3:12b-it-q4_K_M,測試結果對我而言很好。

提示詞如下:

Summarize the following conversation chunk.
只用繁體中文回答.

Return JSON:
{{
  "summary": "...",
  "main_points": []
}}

Text:
{text}

2.1.1.3 extract_claims

extract_claims 一樣利用 gemma3:12b-it-q4_K_M 作為輸入文字的重點提取引擎。

提取出來的重點除了文字以外還有一個 type,這是跟未來替編譯流程加上下一步驟有關:建立重點之間的關聯以建立知識圖譜。

提示詞如下:

Extract atomic claims from the text.

Constraints:
- Each claim must represent a DISTINCT semantic proposition.
- Do not produce multiple claims that can be merged.
- If two sentences imply the same normative rule, merge them.
- Prefer canonical, abstract, policy-level phrasing.
- Maximum 10 claims per chunk.
- Remove rhetorical emphasis.
- No restatements.
- 只用繁體中文回答.

If the text repeats an idea, output it only once.

Return JSON:
{{
  "claims": [
    {{
      "text": "...",
      "type": "normative | descriptive | inference"
    }}
  ]
}}

Text:
{text}

2.1.2 連結流程

連結流程比較簡單,也是類似 c/c++ 程式最後的 link 動作將所有 objs/libs 等連結成一個 exe 或 dll 等。而 Greatest notes compiler 的連結動作則是把所有個別 note 編譯成功後輸出的 json 檔,全部合拼再輸出成一個 Greatest notes 文件。

下面是編譯後的一篇 note 的範例。



留言

這個網誌中的熱門文章

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧

這題數獨(sudoku)題目估計為4+顆星,有點難度。解題需要應用多種技巧,過程非常精彩有趣,是個好題。 底下使用 KillSudoku 作詳細圖解。 1,使用基本排除法則,可以簡單填入6個數字。到此為止,開始使用 候選數法 來解題。如下所示,為填入6個數字後的狀態圖。 2,如下圖,使用進階排除法,在第9列和第4行可以先排除幾個候選數。 3,如圖,在第2行有一個 Naked Subset (3,4),可以對3,4候選數作排除。附帶提一下,反過來看在同一行裡面也可以說有另一個Hidden Subset(2,5,8)存在。Naked Subset和Hidden Subset常是一體二面同時存在,只不過對我們來說,Naked Subset是相對比較容易看的出來。 排除第2行的3,4後,又可以對第2列以外的3作排除,如下圖。 4,接著,在第5行又發現了一個 Naked Subset (3,7,8)。 對第5行三個Subset以外的候選數3,7,8作排除後,又接著產生可以對第5行以外的3作排除。 5,這一題解到此為止,開始進入高潮。大部份能解到3顆星題目的人,猜想應該就此卡住。以下開始需要應用更高級的鍊技巧,才能夠繼續進行。 應用X-Chains鍊技巧,可以找到一條由4條強連結組成的鍊,可以排除候選數2。這裡的鍊指的是由2條以上的強連結組成,而所謂的強連結是指在同一行、或同一列或同一個Box裡,由唯二的候選數構成的連結。如上圖中的第9行中,只有二個2,這二個2構成一條強連結。為什麼說這是一條強連結?因為在這條連結的AB二個端點中,肯定會有一個2存在,要麼是A點要麼是B點。鍊技巧就是將多條強連結串連起來作候選數排除的技巧,而X-Chains是高級的鍊技巧裡面的基本技巧。 接上圖,這樣一來就又可以應用基本排除方法,填入3個數字,如下圖所示。 6,接下來就是本題最精彩的部份,以下需要連續找到3條鍊,才能繼續往下解。 7,找出3條鍊後,剩下來的部份就沒什麼特別的了,只需要應用基本法就能把所有剩餘數字填完。

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

窮人的 AI:自動漫畫分鏡切割

  ( 試試看 ) 在手機上看漫畫時,有一個體驗上的問題: 漫畫原本是「整頁設計」 手機最適合的是「一格一格往下滑」 與其強迫使用者縮放、拖曳、放大,更直覺的做法是: 直接把一頁漫畫自動切成多個分鏡,轉成瀑布流閱讀。 這篇文章分享一個不靠深度學習、完全在前端完成的實作方式: 使用 OpenCV.js 做分鏡偵測 輸出 rect list 再用 全畫面 Canvas 把每個分鏡當成一個「閱讀單位」 整個系統可以拆成三層: 漫畫圖片 ↓ 影像處理(找出 rects) ↓ 排序後的 rect list ↓ 全畫面 Canvas 逐格呈現(瀑布流) Step 1:灰階化 漫畫的資訊 90% 都在線條上,顏色反而是干擾。 cv.cvtColor(src, grayImage, cv.COLOR_RGBA2GRAY); 灰階化的好處: 降低維度 對邊緣偵測更穩定 對黑白漫畫特別有效 Step 2:邊緣偵測,抓出「分鏡的邊」 接下來用最經典、也最夠用的 Canny Edge Detection: cv.Canny(grayImage, edges, 50, 150); 在漫畫中,分鏡外框通常就是最明顯的邊界。 Step 3:形態學操作,把破碎邊框「補起來」 真實漫畫的線條並不完美,常常有斷線、陰影、留白。 所以要做一個很重要的步驟:膨脹(Dilation) const kernel = cv.Mat.ones( 5 , 5 , cv.CV_8U); cv.dilate(edges, dilatedEdges, kernel); 直覺理解就是: 把細線「抹粗一點」, 讓本來斷掉的邊界連成封閉區域。 這一步直接決定後面能不能成功抓到「一整格分鏡」。 Step 4:找輪廓,轉成矩形框(rect) 有了封閉區域之後,就可以找輪廓: cv.findContours( dilatedEdges, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE ); 每一個 contour,代表一個「可能的分鏡區塊」。 接著轉成矩形: const rect = cv.boundingRect(contour); rects.push([rect.x, rect.y, rect.widt...