Skip to main content

Wei Ji

以下從幾個角度分享我對開源軟體的看法。

談作業系統

「自由不是免費的」,這樣的概念大家或多或少都有聽過,不過「自由」本身很抽象而且難以釐清邊界,所以我今天只談開源的自由。

在電腦早期的發展歷史中,作業系統是跟硬體分開來銷售的東西,因為作業系統需要有人來撰寫,有人撰寫就要時間跟勞動,有時間跟勞動就有成本,有成本就要靠銷售來支付,這是經濟運作的 ABC。

但是消費者買電腦就是要用啊?買了卻不能用啟不是很奇怪?於是後來來使興起了捆綁銷售的商業模式,如此一來消費者買到電腦回家組裝完就有作業系統可以使用了。除了作業系統以外,瀏覽器也有類似的情況,微軟的作業系統便靠著這個商業模式佔據了大量的市場份額。

消費者可以無腦的開始使用作業系統、網頁瀏覽器,不過代價什麼?微軟的作業系統一代比一代佔用資源,逼迫消費者購買新硬體,自動更新、「AI」工具在 2026 的今天不斷的騷擾著多數消費者。

「當一個產品是免費的,那不是產品,你才是產品」用來警惕所有貪小便宜的人,不過最惡劣是,消費者實際上在購買電腦時,是有對作業系統支付授權費的。

這是我在 2015 年投入 Linux 懷抱的原因, 是的,在桌面環境下 bug 比較多,因為這是多個各自為政的開源軟體整合在一起,而不是統一由企業開發的; 是的,要操作的順手多少需要會一點指令操作,不比桌面環境來得直觀; 是的,遇到問題要自己爬文,沒有門市或是客服可以求助, 是的,很多主流軟體無法在 Linux 運行,特別是遊戲軟體...

不過我也因此可以自行選擇更新的時機; 我也因此可以在性能比較差的電腦運行系統; 我也因此可以選擇什麼軟體要留在我的電腦上、什麼不要...

我支付了非資本形式的代價,獲得了開源軟體賦予的自由。

談 LLM

都 AI 時代了,誰還談開源精神啊

前幾天我在網路上看到的一段留言是驅使我寫這篇文章的原因,「我啊」我想這麼回答,不過在論述扁平化的社群平台上吵這個沒什麼意義。

在談我的立場以前,我需要解釋這裡至少有兩種看待 LLM 的視角。

info

大眾用語會使用 AI 來稱呼這些基於深度學習的工具,不過它們實際上是指稱基於 LLM (large language model) 的工具,以下我使用 LLM,不過讀者可以自行理解我是在談「AI」。

「AI 好棒棒」視角

這個視角看到的是 Claude Code、Codex...等商業軟體,稍微講究一點人可能是用自己喜歡的工具對接 Claude、OpenAI GPT、Gemini...等商用 LLM API。

這個視角看到的是這些「模型」有很強的解題能力、還是能理解照片或影音檔案的「多模態模型」、可以訂閱吃到飽;經濟實惠。

「LLM 只是工程組件」視角

LLM 最大的問題就是幻覺 (Hallucination),簡單來說就是它會很自信的提供錯誤的資訊,而解決這個問題目前最有效的方法就是建立 RAG (Retrieval-Augmented Generation) 並將相關知識或是潛在答案本身直接和使用的問題一同輸入給 LLM 來獲得穩定且正確的回應。

在 RAG 系統中,LLM 只是組件之一,而不是一切的核心,它的功能是用來揉合和合成文義通暢、好理解的自然語言。又或是在缺乏專用模型或演算法的時候,處理一些文字相關的問題。

我的觀點與分析

要讓一個 RAG 系統堪用,至少需要三個要素:

  • 一定水準之上的 LLM 組件
  • 維護優質的知識庫
  • 有效的演算法(RAG 策略)實現

生態系內已經有不少開放權重的 LLM 模型,llama.cpp 與 GGUF 努力,模型量化以及允許使用多種運算後端,大幅的降低 LLM 的記憶體門檻以及擺脫 CUDA 的供應商鎖定,這是開源陣營的一大進展。

「不過為什麼這些開放權重模型表現的一般般,不如商業方案來得可靠?」 「是不是因為這些模型是削弱版的?」 「那我還是付錢買商業『模型』好了」 以上可能是不少人的想法,那是因為它們是透過第一種視角看事情。

眼尖的讀者可能發現我在談論商業 LLM 時,「模型」是括弧起來的,因為我採用的是第二種視角,這些運算都藏在 API 之後,它很有可能已經實作了一個完整的 RAG 系統,API 表現並不等於模型表現,我不管這些企業怎樣宣傳它們的「模型」,直到我看到開放權重的模型以前,我都預設它是一個 RAG 系統而不是模型。


「RAG 已死」或是基於 Agent 的軟體策略,在我看來也都是第一種視角下的世界。

info

建議讀過我之前解釋 Agent 的文章:

Agent! Agent! Agent! 所以 Agent 到底是什麼?

會對整個上下文有更清楚的了解。

「RAG 已死」的主張者認為,新型的「模型」具有更大的上下文窗口,我們只要把大量的資料倒入,「模型」就能自己解決問題。

Agent 的軟體策略則是透過建立一個迴圈,每一步都給予「模型」更多的資訊,「模型」最終就能解決問題。

你發現問題了嗎?如我稍早提過得,一個良好的 RAG 系統至少需要具備:LLM、策略實作與知識庫。

這些人一邊抱怨開放權重的 LLM 不夠好用,一邊轉向使用商業「模型」,然後絲毫不考慮改進策略與維護知識庫。

在我眼中,這種作法只會讓自己成為企業的待宰羔羊。

你每一次的消費,都是在為你想要的世界投票

Every time you spend money, you're casting a vote for the kind of world you want

試想一下一個高度仰賴閉源 API 的世界是怎麼運作的。

硬體閉源循環

目前深度學習生態系建立在 CUDA 之上,供應商業 API 的企業自然使用 Nvidia 的硬體來避免遷移成本,Nvidia 的 HPC GPU 需求變高,Nvidia GPU 的市場行情變高,消費者買不到便宜的消費級 Nvidia GPU,於是選擇使用商業 API,循環發生了。

這就是為了開源陣營積極發展通用型運算後端,如 llama.cpp 的 Vulkan 後端;ONNX 的 WebGPU 後端。這也是為什麼我會對於通用 GPU 進行深度學習運算特別執著。

消費性循環

「RAG 已死」與 「Agent 流派軟體」,傾向單純的提高資料吞吐量而不是優化算法,意味著更多的 Token,意味著更多的運算,更多的運算代表更多的 GPU 與記憶體需求,這些企業不斷的擴建資料中心,消耗 GPU 和記憶體市場的產能,造成 GPU 與記憶體價格飆漲,消費者買不到設備,於是轉而使用商業 API,循環發生了。

這就是為什麼我主張應該強化知識庫以及 RAG 策略,而不是把所有事情都外包給商用「模型」。

小結

「使用開放權重模型」「尋找某種標準化資料使其能在知識庫系統之間轉移」「繼續精進 RAG 而不是使用 Agent Tool」我的立場看似違背大趨勢、逆風、固執且無理,為什麼?因為:

自由不是免費的。

Wei Ji

工欲善其事,必先利其器

當要和(主觀視角)未知的檔案格式打交道的時候,必定要先準備好:開啟檔案的軟體與檔案樣本。

info

「開啟檔案的軟體」可透過 Editor, Reader, Browser, Viewer ...等關鍵字尋找。

從領域驅動開發 (DDD, domain-driven design) 的視角來看,針對特定用例設計的檔案格式其實封裝了大量的領域模型,而能打開檔案的軟體則是提供最直觀的方式讓人理解這些領域模型,這些軟體可以說是領域大門的鑰匙。

檔案樣本是用來驗證多個軟體可靠性的,因為在開源的世界裡沒有「唯一解」只有「相對有用解」,在某些情況甚至需要同時使用多個軟體,因為每一個軟體可能是針對特定的用例設計。

以下舉幾個我過去實際接觸過得例子。

大圖輸出、廣告帆布 - PDF

「列印要匯出成 PDF 檔案」大概是這個時代的常識,不過如果是「10 公尺×10 公尺」這種 PDF 呢?不幸的是一般的開源 PDF 瀏覽器無法正常瀏覽這種類型的 PDF,而且這種 PDF 的樣本也不好取得。

Inkscape 屬於向量繪圖軟體; LibreOffice Draw 仍是偏向辦公文書處理的圖像軟體; Okular, Gnome Evince 是 PDF 瀏覽器,但是無法正常在「10 公尺×10 公尺」這種檔案中縮放瀏覽。

上述軟體或多或少都能處理 PDF 的部份用例,但是就是無法覆蓋「10 公尺×10 公尺」這種 PDF,原因是這是一個名為桌面排版軟體 (DTP, Desktop publishing) 的領域。

Scribus 的定位則剛好屬於這個領域,因此用它可以輕易的產生與遊覽「10 公尺×10 公尺」這樣的樣本。

info

以上資訊源自於和廣告帆布業者相關的軟體開發經驗。

GIS - GeoJSON

GeoJSON 是 GIS (Geographic Information System) 使用的經典檔案交換格式。

info

Mapbox 的 Vector Tile 本質上是透過 Protocol buffers 封裝的 GeoJSON。

從規範的層面你可以去翻閱 RFC 7946,從函式庫的層面可以使用諸如 @types/geojson 來獲得封裝好的 Typescript 界面。

OSM (OpenStreetMap) 本身是一個豐富的 GIS 資料來源,QGIS 本身是一個用於瀏覽與編輯 GIS 資料的軟體,同時能夠透過它從 OSM 提取特定地區的資料並儲存成 GeoJSON1

線上的工具也有像是 geojson.io 這樣的網站可以產生與預覽小型的 GeoJSON 資料。

info

以上資訊源自於林木業 GIS 解決方案相關的軟體開發經驗。

Web3D - glTF

如果你想要在網頁上渲染一個 3D 模型,將模型輸出成 glTF 是標準方案之一,通俗的描述這個檔案格式為「3D 的 JPEG」。

glTF 的檔案樣本:

https://github.com/khronosgroup/gltf-sample-models

glTF 的線上瀏覽器:

https://gltf-viewer.donmccurdy.com/ (較舊) https://github.khronos.org/glTF-Sample-Viewer-Release/ (較新)

當然,glTF 不只用於 Web 應用,你也可以使用其他軟體匯入或是開啟,但是當要開發 Web3D 應用時,3D 的渲染能力受限於遊覽器,因此使用上述兩個線上(基於瀏覽器)的瀏覽方案更能一併測試「在瀏覽器 runtime」下的渲染效果。

info

以上資訊源自於基於 Three.js 應用程式的開發經驗。

機械工程 - CAD

我拿一般平面的圖檔來比喻,常見的圖檔這這幾種,具有不同的特性:

  • JPG:有損壓縮點陣圖,用於呈現給人類看,實際上有部份資訊會丟失。
  • PNG:無損壓縮點陣圖,能夠保留原始像素資訊。
  • SVG:向量圖,不像點陣圖放大後會變成馬賽克,向量圖以幾何參數的方式儲存影像。

前面介紹的 glTF 可以說是 3D 的 JPG,而 STEP 則是 3D 的 SVG。一種典型的 3D 描述方式是用三角面近似各種幾何體,就像用多邊形近似圓形這樣,但是在工業製造這種對精細度有要求的產業中,用這種近似方式滿足產業需求的話需要非常大量的三角面,檔案儲存效率非常差勁,所以會用 STEP 這樣的檔案,以儲存幾何參數來交換 3D 模型。

以下是可獲得一些 STEP 檔案樣本的地方:

可以打開 STEP 的線上工具也不少:Online3DViewer、Autodesk Viewer、Onshape。本機軟體則有像 FreeCAD 這樣的軟體可以用來打開 STEP 檔案。

不過 STEP 是一個難搞檔案格式,它有很多細部的規範:

AP 編號名稱說明
AP203Configuration controlled 3D design
AP210Electronic assembly design
AP214Automotive mechanical design
AP218Ship arrangement (造船)
AP219Electrical harness design
AP220Process plans and resources
AP232Mechanical product definition (tolerance)
AP238STEP-NC (CNC加工)
AP242Managed model-based 3D engineering

即便是像 Autodesk 或 Onshape 這種專門在這個領域深耕的公司,它們的軟體也會有一些細部特性不支援。

程序化操作 STEP 檔案的函式庫則有 OCCT (Open CASCADE Technology),不過它的兩個 Python binding (pyocctpythonocc) 都僅支援 conda 安裝,為了在符合 Python 標準的環境下運行可能需要混合使用 micromamba 和 venv 等工具將 conda 的套件和 PyPI 的套件放在同一個生產專案中使用。

額外補充,FreeCAD 本身有一個 Python 的終端機,可以用來腳本化操作 CAD 軟體,同時也可以透過一些方式把這些函式庫從 Python 直接 import 進來使用。要注意的是 1.0.0 前後組織 Python 函式庫的方式不太一樣。

info

以上資訊源自於對接製造業相關需求的原型軟體開發經驗。

三維重建 - 點雲

前面介紹過兩種 3D 模型,但是它們皆產自數位世界,如果我們想要從現實世界捕捉三維物體的資訊呢?

我們會透過 ToF 攝影機或光達 (LiDAR) 之類的工具來掃描現實世界的物體,這些資訊會以點雲 (point cloud) 的形式儲存。

CloudCompare ViewerMeshLab 是可以用來瀏覽點雲檔案的工具。

不過對於缺乏專用設備的人而言,有另外一條路徑:SFM (Structure-from-Motion),可以從二維影像回推三維資訊。

SFM 的經典工具為 colmap,你只要手上有一段環視物體或場景的影片,透過 FFmpeg 切成若干個圖片,再用 colmap 運行 SFM 流程,就能獲得點雲檔案了。

info

以上資訊源自於對接製造業相關需求以及感測器設備週邊軟體開發的經驗。

3D 列印 - G-Code

info

熔融沉積成型 (FDM) 跟光固化的生態不太一樣,以下以 FDM 的角度描述。

在 3D 列印中,會需要將一個僅有形體的幾何資訊轉換成可以被列印機的指令碼,這個過程稱為「切片」。PrusaSlicerCuraSlic3r...這些用來切片的軟體則被統稱為「切片軟體」。

G-Code 原本是被用於 CNC 這種切削工藝的指令碼,不過該概念與技術被 3D 列印機重複使用,作為執行列印的指令碼。G-Code 檔案預覽起來會變成由線條構成,並且一層一層的畫面。

info

以上資訊源自於對接製造業相關需求以及學生時期課餘接觸 3D 列印的經驗。

其他 3D 檔案與工具

如果你需要和 3D 美術合作,手邊有一個 Blender 永遠不會是壞主意,你可能需要處理諸如 FBX、OBJ 之類的檔案。

FBX 是 Autodesk 的專有檔案,標準的操作方式是使用 Autodesk 官方的 FBX SDK,不過你或許可以在 GitHub 上找到像 fbx-tree-view 這樣的野雞軟體來檢查內部的樹狀結構。

assimp 是 3D 檔案的瑞士刀工具,不過正如我先前介紹的「3D 檔案」的複雜性,使用前還是需要先讀過它的文件描述的對各種檔案格式的支援性

CSG (Constructive solid geometry) 是一種透過布林運算進行 3D 建模的技術,如果你有實時根據參數生成 3D 模型的需求,在 Three.js 的生態系有 three-bvh-csg 這樣的函式庫可以處理。

info

three-bvh-csg 中有一些演算法優化相關的問題被懸賞喔~想賺外快的朋友不妨參考看看。

小結

在大型組織中,領域專家通常是內部人員;在正常的開發情境中,領域通常也是一個相對穩定的空間,因為需求相對清晰。不過在一些特殊的條件下,我需要面對的是來自各種千奇百怪領域的需求。早在接觸 DDD 以前我就已經使用各種開源軟體作為領域指南針,直到接觸 DDD 後,我終於可以把這個概念明確的用「領域對齊」這個詞表達了。

從前面的例子中,我們可以知道哪怕只是「3D 檔案」都有非常多種似是而非、截然不同的可能,在開發之前如果不先進行領域對齊,可想其後果有多嚴重。

Footnotes

  1. How to download OSM data using QuickOSM Plugin in QGIS. Retrieved 2026-04-15, from https://www.giscourse.com/how-to-download-osm-data-using-quickosm-plugin-in-qgis/

Wei Ji
info

實際上花了一兩天摸索之後,這個坑大概是填不完了,但是姑且還是紀錄一下。

起因

最近在 104 看到這樣一個職位描述:

Digital Twins Engineer

Scope of Work

  • Integrate outsourced BIM/3D models into the ██████████ platform.
  • Develop capabilities to bind dynamic data (e.g., IoT sensor data, system states) to 3D models and visualize them.
  • Embed LLM use-cases into DT scenarios—e.g., natural-language queries for asset status, simulation results, or triggering operations.
  • Partner with frontend/UX to optimize DT UI/UX and 3D rendering.
  • Work with backend/data teams to ensure efficient DT data flows.
  • Participate in DT PoCs to explore and realize innovative applications.
  • Ensure DT solution performance, stability, and scalability.

Qualifications

  • Proficient in at least one backend or frontend language (e.g., Python, C#, JavaScript/TypeScript).
  • Knowledge of 3D graphics and at least one 3D engine/library (e.g., Unity, Unreal Engine, Omniverse, Three.js, Babylon.js).
  • Experience with data integration and APIs (RESTful, WebSocket).
  • Understanding of BIM/CAD formats (e.g., IFC, Revit API) and processing workflows.

你知道嗎?我本來就想在我的房間搞數位孿生了,看來這就是我的下一個題目了!

工欲善其事,必先利其器

正如我在前一篇文章(從 DDD 看檔案格式)中提到的,打開領域的第一步是先找檔案跟軟體。

從職缺內容來看 IFC (Industry Foundation Classes) 應該就是這次的重點關注檔案格式了,這些地方可以找到 ifc 的檔案樣本:

並且目前看起來最可靠的開源編輯器方案是 Bonsai (原名 BlenderBIM),它是一個 Blender 插件,IFC 的相關實作則是由 IfcOpenShell 提供。

接下來就是用 Bonsai 繪製我的房間了嗎?我可不這麼認為。

Scan to BIM

現在是 2026 年;「AI」依然大行其道;泡沫尚未被戳破;X 上充滿了高斯潑濺、實景掃描、三維重建的貼文,IFC 總該能透過演算法從點雲自己生出來了...吧?

事實是,就算撇開 AI 不談,Scan to BIM 也已經是在 AEC (Architecture Engineering Construction) 業界內存在已久的詞彙,用來描述「從現實掃描並建立 BIM (Building Information Modeling)」的過程,不過傳統上這個過程可能是指:把點雲匯入軟體後,由工程師拉出批配的模型,比較接近機械逆向工程的作法。

OK,所以接下來就是要研究 Scan to BIM 的方案了對吧?

我:( ՞ټ՞)

事情是這樣的,在 AEC 領域,掃描通常由 ToF 攝影機或光達 (LiDAR) 這樣的專門設備完成,而我只能透過 SFM (Structure-from-Motion) 的手段獲得點雲。SFM 使用二維圖像回推三維資訊的技術,講白化來說就是我可以用手機拍影片、切成一堆圖片、經過 SFM 處理來獲得點雲。

高斯潑濺 (3DGS, 3D Gaussian Splatting) 可以向一個點雲填充額外的機率資訊,最後呈現出一種現代 3D 渲染技術所不能企及的寫實度,就像一張 3D 的照片,是近幾年日漸熱門的一種技術。因為 SFM 是主流高斯潑濺資料處理流程的一部分,加上這是一個我之前就有興趣但是沒有花心思跑過的技術,所以我打算在執行 Scan to BIM 之前先試著跑一次 SFM 到高斯潑濺的路徑。

SFM

SFM 的經典工具是 colmap,比較遺憾是它在 GitHub 的 release 中預編譯的 release 只有 Windows 的,並且計算過程仰賴 CUDA,2024 年使用它的時候可是稍微折騰了一番。

不過「去 CUDA」是我目前的主要原則,姑且還是在沒有 CUDA 的環境硬著頭皮編譯跟跑下去了,讓我有點意外是,無 CUDA 版本依然會使用 GPU 加速:

不知道這是本來就有;但是我在 2024 遺漏掉的特性,還是這是近一兩年內新加入的能力。

整個流程大概是這樣:

mkdir -p output
ffmpeg -i video.mp4 -qscale:v 1 -qmin 1 -vf fps=2 ./images/%04d.jpg

mkdir -p ./data
colmap database_creator --database_path ./data/database.db

colmap feature_extractor \
--database_path ./data/database.db \
--image_path ./images

colmap exhaustive_matcher \
--database_path ./data/database.db

mkdir -p ./data/sparse
colmap mapper \
--database_path ./data/database.db \
--image_path ./images \
--output_path ./data/sparse

colmap bundle_adjuster \
--input_path ./data/sparse/0 \
--output_path ./data/sparse/0 \
--BundleAdjustment.refine_principal_point 1

colmap gui \
--database_path ./data/database.db \
--image_path ./images \
--import_path ./data/sparse/0
info

話說回來,我之前使用過得經驗其中幾次是針對 360 影像修改過得 fork (json87/spheresfm)就是了。

高斯潑濺

試著用了比較有名的實作 nerfstudio

ns-process-data video --data source.mp4 --output-dir processed_data

ns-train splatfacto --data processed_data

第一個指令實際上是 colmap 的封裝,所以還是要先安裝 colmap。比較遺憾的是第二個指令高度仰賴 CUDA。來回翻了一下其他方案,全部都仰賴 CUDA。

Taichi Lang 是一個 GPU API 的高級封裝,允許開發者可以用較簡單的程式使用 GPU 進行平行化運算,並且支援 Vulkan 作為運算後端1

後來我找到了 wanmeihuali/taichi_3d_gaussian_splatting,一個使用 Taichi 的高斯潑濺實作,然而其程式碼依然使用了不少 PyTorch + CUDA,感覺去 CUDA 的成本依然很高,因為我對 GPU 平行化運算跟機器學習的程式相對陌生,所以試著玩高斯潑濺的念頭至此打消了。

Scan to BIM

建了一個口袋名單之後抽從星星數比較高的抽出來看,打開 LTTM/Scan-to-BIM 就看到滿滿的:

'cuda' if torch.cuda.is_available() else 'cpu'

因為沒有 CUDA 就不讓我用 GPU 加速,那我乾脆不要用好了,下一個。

VaclavNezerka/Cloud2BIM 雖然沒看到 CUDA 的影子,不過它有其他問題,它在 README 提供的點雲檔案是 Kladno station:

然而預設組態與論文內使用的是 Hotel Opatov:

看起來是一個結構與格局相對方正的建築,讓我不禁懷疑這個算法處理像我房間這種堆放各種物品空間的可靠性,更重要的是它的 README 還寫著:

This repository contains the foundational open-source research version of the Cloud2BIM algorithm. An advanced, production-ready version of this software, Cloud2BIM-AI, is now available through Constriq (a Czech Technical University spin-off).

看來這是典型的「開源的心不甘情不願」的那種專案。

Footnotes

  1. Taichi 和 PyTorch 有哪些相似和不同? - 知乎. Retrieved 2026-04-15, from https://www.zhihu.com/question/535601383

Wei Ji

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

本文僅覆蓋「連續性測試」相關的主題。

連續性測試

下述內容與其他文章有高度關聯,缺乏上下文的情況可能很難理解。

關於通用 GPU 加速與標準化請見:

Qwen3 TTS 之旅:語音嵌入

關於資料視覺化請見:

Qwen3 TTS 之旅:資料視覺化


採集兩個人的聲音(例如:一男一女),並對其進行嵌入運算得到嵌入向量,再兩個向量進行線性內插。

將內插的數個向量分別給予 Voice Clone 模型進行運算,會得到「女 100% + 男 0 %」、「女 90% + 男 10 %」...的聲音,如果空間是連續的,理應得到一個漸近改變聲音的過程。

這是一個進行視覺化以前就應該先進行的基本測試,不論是 AI 還是教授(?)都建議我先做這個測試。之所以一拖再拖的原因是我尚未完成 TTS 相關程式基礎設施的建立,包含通用 GPU 加速與標準化。所以不是很想在實驗內加入需要運行太多次 TTS 的步驟。

不過也因為這個測試意外的讓我發現嵌入伺服器實作有 bug。

測試結果

它並沒有很明顯的漸進,因此我們可以知道特徵空間不是線性的,不過也沒有出現「非人聲音」或是 TTS 故障的現象,可以肯定空間至少是連續的。

至於女聲跟男聲在 60~70%時劇烈變化,問題原因可能在於兩個樣本處於空間的位置有關,

如果女聲的樣本距離男聲區很遠,自然大部份內插值都落於女聲區。

Wei Ji

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

並且預計是系列文章的收尾。

標準化

關於模型組件的標準化細節請見:

Qwen3 TTS 之旅:語音嵌入

簡單來說,在在我的工作流程中,一個模型的標準化代表著:

  • OCI (Open Container Initiative) 映像檔封裝,可以透過 Docker 或其他相同技術佈署。
  • 可以透過 OpenAI-Compatible API 呼叫。
  • 通用 GPU 加速,使用 Vulkan、WebGPU...等通用 GPU 界面實現硬體加速,避免被硬體供應商鎖定。
  • 使用 Hugging Face SDK,除了分離程式碼與模型檔案管理的邏輯以外,可透過設定 HF_ENDPOINT 等參數指向鏡像站獲得本地下載加速。

從而將使用者的認知負荷降到最低,不論是實驗還是產品研發,調用者不用花費太多經歷在配置模型,這才符合我對「可用模型」的標準。換句話說,使用 CUDA 加速、凌亂的 Python 程式碼皆未達該門檻。

Qwen3 TTS 的 ONNX 移植

ONNX 是目前我覺得最可靠的格式,雖然可以想像轉檔相關的工具必定存在,只是我還是稍微搜尋一下看有沒有現成實作,畢竟能不自己寫程式就不自己寫(?)

DLL、C#、量化的方案不考慮,刪去法剃除後就只剩下 xkos/Qwen3-TTS-12Hz-1.7B-ONNX 看起來比較可行,但是它是 Hugging Face 上的野雞專案,作者也沒有提供足夠多的資訊構成公信力:

模型跟程式碼沒有分離,通通放在 Hugging Face 上,專案下也沒有足夠多的討論,這對我而言不夠成足夠多的可信度。

另一方面,xkos 的實作不知道為什麼比 Qwen3 官方的操作方式來得複雜,需要「預先生成」一些檔案。

安全性檢查

綜合上述上傳者缺乏公信力,程式碼又稱不上乾淨,模型相關的封裝全部擠在一個 1.3k 行的程式內,很難一眼看出有什麼問題,於是我用 opengrep 做了一個簡單的掃描:

$ opengrep scan --config auto . -v

synthesize.py
❯❯❱ trailofbits.python.pickles-in-numpy.pickles-in-numpy
Functions reliant on pickle can result in arbitrary code execution. Consider using fickling or
switching to a safer serialization method
Details: https://sg.run/ryKe

64┆ data = np.load(args.speaker, allow_pickle=True)

tts_engine.py
❯❯❱ trailofbits.python.pickles-in-numpy.pickles-in-numpy
Functions reliant on pickle can result in arbitrary code execution. Consider using fickling or
switching to a safer serialization method
Details: https://sg.run/ryKe

559┆ data = np.load(cache_path, allow_pickle=True)

掃到的兩個 pickle 都是前面說的「預先生成的檔案」,可能問題不大。ONNX 本身似乎也有風險1,但是已經是相對安全的模型格式。

info

沒用 semgrep 的原因是因為:

$ podman run --rm -v "$PWD:/src" docker.io/semgrep/semgrep:1.157.0-nonroot semgrep ci
run `semgrep login` before using `semgrep ci` or use `semgrep scan` and set `--config`
There were errors during analysis but Semgrep will succeed because there were no blocking findings, use --no-suppress-errors if you want Semgrep to fail when there are errors.

開源軟體還要我登入?相關的故事可以在 Rddit 的討論上看到2

重構與壞味道

info

以下不是針對 xkos,而是就程式碼本身進行探討。因為他至少也是把程式跟模型上傳給別人使用了,也有標記 Qwen3 TTS 原本的開源許可證,所以以下就稱呼他為「熱心鄉民」。

為了封裝成 API 伺服器,我需要對熱心鄉民的程式碼進行重構,抽出我用得上的邏輯,因為並不是所有 Qwen3 TTS 的模型我都需要,我只需要負責 Voice Clone 的模型。

程式碼與模型未分離

即便是 Qwen3-TTS 官方也是採取程式碼模型分離的策略。

熱心鄉民則是將程式碼與模型一同上傳到 Hugging Face,並且手刻 os.path.join 來讀取模型,同時將未量化模型與量化模型放在同一個 repo 內。

Qwen3-TTS 官方至少也是採取 1.7B 和 0.6B 兩種大小的模型分開來放的措施。

透過 Hugging Face SDK 實現程式與模型解偶除了我前面提到的可以用環境變數指定鏡像來源以外,還能在程式內實作「有用到才下載」的邏輯,這種整包上傳的方式,不是只能用 Git LFS 一次下載就是要一個手動挑選檔案,不管怎樣都不是應該發生在 Hugging Face SDK 已經成為實質產業標準的現代,屬於非常不成熟的作法。

違反使用直覺的封裝

在 Qwen3-TTS 的官方實作使用模型是像這樣的:

Sample Code
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
device_map="cuda:0",
dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
)

# single inference
wavs, sr = model.generate_custom_voice(
text="其实我真的有发现,我是一个特别善于观察别人情绪的人。",
language="Chinese", # Pass `Auto` (or omit) for auto language adaptive; if the target language is known, set it explicitly.
speaker="Vivian",
instruct="用特别愤怒的语气说", # Omit if not needed.
)
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
device_map="cuda:0",
dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
)

# single inference
wavs, sr = model.generate_voice_design(
text="哥哥,你回来啦,人家等了你好久好久了,要抱抱!",
language="Chinese",
instruct="体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。",
)
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-Base",
device_map="cuda:0",
dtype=torch.bfloat16,
attn_implementation="flash_attention_2",
)

ref_audio = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone.wav"
ref_text = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."

wavs, sr = model.generate_voice_clone(
text="I am solving the equation: x = [-b ± √(b²-4ac)] / 2a? Nobody can — it's a disaster (◍•͈⌔•͈◍), very sad!",
language="English",
ref_audio=ref_audio,
ref_text=ref_text,
)

每一種模型的使用都非常直觀。

反觀熱心鄉民的程式長這樣:

# Step 1: Generate Global Cache (one-time) 
python generate_cache.py --model_dir ./model

# Step 2: Create Speaker Profile (once per voice)
Step 2: Create Speaker Profile (once per voice)
python create_speaker.py \
--model_dir ./model \
--ref_audio reference.wav \
--ref_text "Transcript of the reference audio" \
--language english \
--output ./speakers/my_voice.npz

# Step 3: Synthesize Speech
python synthesize.py \
--model_dir ./model \
--speaker ./speakers/my_voice.npz \
--text "The weather is wonderful today." \
--output output.wav

讓我無法理解的是 generate_cache.py 的設計,如果這是一個 pure function 每次都產生一樣的東西,那為什麼不乾脆做成模型直接載入,更別提為什麼 Qwen3 TTS 的官方實作就沒有這個問題?

當然,這可能是我對 ONNX 的理解還不夠深入而產生的疑問。

上帝物件

在熱心鄉民的程式碼中,tts_engine.py 總計 1.3k 行的程式碼中包含了一個 1k 行的 Qwen3TTSONNXInference class,同時處理了以下幾種模型:

  • 16f量化/原始
    • speaker_encoder.onnx
    • speech_tokenizer_decoder.onnx
    • speech_tokenizer_encoder.onnx
    • Vocie Design/Voice Clone
      • code_predictor.onnx
      • code_predictor_kv.onnx
      • text_embedding

至少 18 種排列組合,表面上在遵守 DRY 原則:避免重複撰寫載入模型的程式,但是實際上卻因為缺乏抽象,內部充滿大量的 if 判斷式,讓整個實例的狀態與可能性變得非常臃腫且不好輕易理解狀態變化。

舉例來說 def get_codec_embedding(self, input_ids: np.ndarray) -> np.ndarray: 的 call stack 需要追朔如下這麼多層才知道到底是用哪一個模型計算的:

# self.get_codec_embedding
# self.codec_embedding
# models["codec_embedding"]
# models = self._vc_models
# self._vc_models = self._load_talker_set(self.onnx_vc_dir, "voice_clone")
# self._load_talker_set
# result[name] = ONNXInferenceSession(path, self.use_gpu)
# ("codec_embedding", "codec_embedding.onnx"),

某某Service某某Repository 這在後端的軟體開發中是十分常見的模式,就算不使用反轉注入或工廠模式那種高深的 OOP 技巧,單純的根據職責切割、輾平也不會寫出這種毫無工程美感的程式碼。

info

我知道實務上超過一萬行的 class 並不罕見,但是以現代開發的建議來說,500 行以上就算多了3

可疑且意義不明的模型拆分

最後我終於碰到了放棄前的最後一個障礙:

(…)main/onnx/voice_clone/talker_decode.onnx: 100%|████████████████████████████████████████████████████████████████████████████████████████| 1.72M/1.72M [00:05<00:00, 308kB/s]
2026-04-05 22:51:44.604204820 [W:onnxruntime:, session_state.cc:1327 VerifyEachNodeIsAssignedToAnEp] Some nodes were not assigned to the preferred execution providers which may or may not have an negative impact on performance. e.g. ORT explicitly assigns shape related ops to CPU to improve perf.
2026-04-05 22:51:44.604225200 [W:onnxruntime:, session_state.cc:1329 VerifyEachNodeIsAssignedToAnEp] Rerunning with verbose output on a non-minimal build will show node assignments.
2026-04-05 22:51:45.667070708 [E:onnxruntime:, inference_session.cc:2600 operator()] Exception during initialization: filesystem error: cannot get file size: No such file or directory [/home/flyskypie/.cache/huggingface/hub/models--xkos--Qwen3-TTS-12Hz-1.7B-ONNX/snapshots/6023a58eba391c4e2dbe7ff2dd73fbc9f039c76e/onnx/voice_clone/layers.10.input_layernorm.weight]

熱心鄉民的 repo 內充滿了這種意義不明的小檔案:

原本我想說可能是上傳了一些中間文件,可能最後根本用不到,事實證明這些檔案是有仰賴關係的。

我試著用 netron 觀察 ONNX 檔案的模型結構,但是並沒有太大幫助:

注意右邊和下方的捲軸塊,顯示這是一張十分巨大的結構圖,可見這個 ONNX 模型似乎以非常不自然的方式被呈現。

接下來合理的方案應該是放棄熱心鄉民的實作,直接自己轉 ONNX,不過想必有額外不少知識需要理解,也不知道需要花多少時間,於是我決定這個旅途先到此為止。

小結

過程中的幾個 ETL 步驟的程式碼我整理過之後上傳 GitHub 歸檔了:

https://github.com/FlySkyPie/qwen3-tts-etl

這一系列文章其實也是寫給我自己的紀錄,方便過一陣子之後回來處理這個主題的時候可以透過文字回憶一下細節。


在「序」中,我提到了整個旅途是因 Qwen3 Voice Embedding 而起,不過實際上還有其他原因。

我對於包含建造仿生人形機器人在內以及 TTS 等技術都有興趣,這個部份的情感與思緒比較複雜,改天有機會再談。今天先談「資料專案」的部份。

工作的時候因為公司內部的 AI 專案做準備,當時讀了不少資料,其中一個鐵人 30 天系列我很推薦:

吵什麼 AI 煉金術?!你家有礦嗎?(資料領域必知的 30 個詞彙) :: 2023 iThome 鐵人賽

雖然內容缺乏組織,但是我認為建立基本概念以及提供足夠多的領域關鍵字上是十分適合的入門材料,ETL 跟資料專案的概念我就是從這系列文章為起點建立起來的。

求職未果後,我便想著我似乎還沒跑過一次資料專案,手上剛好有著一個資料集以及可以用來玩它的嵌入模型,就想著當著模擬跑一遍流程試試看,反正 S3 實例、PyPi 鏡像、Hugging Face 鏡像...等等資料專案大概會用到的基礎設施我都準備好了。

很遺憾花了兩個多禮拜並沒有達到我一開始預期的進度,不過我還有其他主題需要處理,只好先寫文章把結果做個整理之後先告一段落了。

Footnotes

  1. LobotoMl/ONNX_runtime_hacks at main · alkaet/LobotoMl. Retrieved 2026-04-07, from https://github.com/alkaet/LobotoMl/tree/main/ONNX_runtime_hacks

  2. Opengrep - a truly Open Source fork of the Code Security tool Semgrep - Announced : r/devops. Retrieved 2026-04-07, from https://www.reddit.com/r/devops/comments/1i83yde/opengrep_a_truly_open_source_fork_of_the_code/

  3. max-lines - ESLint - Pluggable JavaScript Linter. Retrieved 2026-04-07, from https://eslint.org/docs/latest/rules/max-lines

Wei Ji

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

資料處理的幾個前置作業請見:

本文僅覆蓋「使用主成份分析進行資料視覺化」相關的主題。

PCA

主成份分析 (PCA, Principal component analysis) 簡單來說是一種對資料進行線性降維的技巧。

在開始寫程式之前我有稍微學習一下相關理論,學習筆記放在其他文章,細節我就不再此贅述:

Qwen3 TTS 之旅:流形學習

如果知道 PCA 原理的話大概可以直觀的發覺它不適合直接套用在 140k 筆 2048 維資料上,實際上可能需要使用增量 PCA (Incremental PCA),並搭配 joblib 實現持久化與斷點續傳之類的機制。

不過為了驗證整個流程的可行性,在正式跑以前,我先抽比較小的樣本直接跑 PCA,或是搭配其他非線性算法。

抽樣失敗

以下是我第一次嘗試,抽樣 1000 個資料跑 PCA 的結果:

我原本以為是資料原本特性的關係,所以試著跑 PCA 降到 50 維,再跑幾個非線性的降維:

不過結果都不太理想。

累積解釋變異圖

累積解釋變異圖 (Cumulative Explained Variance) 是根據特徵值大小推論不同維度主成份的「資訊量」(或理解為「影響力」):

綠色是每一個主成份的資訊量佔比,紅色曲線是把前 N 個主成份的佔比累計,上述的「取 50 維」是根據這個圖表中佔比 75% 對應的主成份數得來的。

抽樣成功

後來在做連續性測試的時候才發現嵌入伺服器實作有問題,簡單來說就是 Garbage in, garbage out 的具體例子,細節不在此贅述,請見其他文章:

Qwen3 TTS 之旅:語音嵌入

修復後重新跑一遍終於正常了,男聲和女聲的差異在三個主成份以內就能分離:

Wei Ji

前情提要

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

本文僅覆蓋「資料預處理」相關的主題。

Mozilla Common Voice

Common Voice 是 由 Mozilla 基金會所发起的群眾參與專案,旨在為語音辨識軟體建立自由資料庫,並且主要宗旨為建立多樣化的語音樣本,因此裡面也包含了一個台灣語音資料集,2023 年的時候我就以防萬一下載了一份放在手邊。

關於 Mozilla Common Voice 我認為有一點值得一提,2024 年的時候我收到這樣一封電子郵件:

大意就是語音資料的貢獻者想要撤銷貢獻,因此 Mozilla 通知下載過資料集的人(也就是我)刪除對應的資料。

先不談這個機制是否對下載的人有實質約束力,至少在這個「AI 公司」在網際網路上掠奪數據與資料,致個人的資料權利於無物的時代,Mozilla Common Voice 可以說是一股清流,至少我持有的資料是這些志工自願貢獻的,在法律與道德上我皆有權利在合理的範圍內自由使用。

然而我並沒有對 2023 年的資料做適度的處理,也很難從中挑出要被撤銷的檔案,於是我直接重新下載了一份 2026 版的。

資料集與預處理

Mozilla Common Voice 2026 (cv-corpus-25.0-2026-03-09/zh_TW) 內容物大致上長這樣:

├── clips/*.mp3
├── validated_sentences.tsv
├── unvalidated_sentences.tsv
├── other.tsv
├── validated.tsv
└── invalidated.tsv

首先映入眼簾的是 .tsv 檔案,它們提供了標籤資訊,也就是某個 .mp3 是誰※貢獻的、什麼性別、什麼年齡區間、字稿...。

info

資料集本身是使用去識別化的 id 去辨識貢獻者,同時 Mozilla Common Voice 的使用者授權也明確的要求下載的人禁止對貢獻者進行再識別,所以若後續文章我需要描述「某人」時,我會使用專案內部重新生成的 id,而不是資料集本身提供的 id。

這是一個尚未經過正規化 (Normalization) 的資料,如果我要讓應用程式能夠更方便的提取特定貢獻者的資料或是其他標籤(性別...等),勢必需要使用關聯資料庫,並對資料進行正規化。

另外一個問題是該資料集包含 140k 筆語音資料,也就是 140k 個 .mp3 檔案,而這種在檔案系統上大量且碎片化的檔案在進行遷移(複製)時非常耗時,

因此我先把這些原始資料預處理成 SQLite 檔案,並用 BLOB 資料欄位儲存 .mp3

Wei Ji

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

本文僅覆蓋「資料集嵌入」相關的主題。

大型資料處理

即便在 ONNX 中使用 WebGPU 實現硬體加速,資料集包含 140k 筆資料,逐一進行嵌入依然需要歷時兩個小時左右。

info

關於嵌入伺服器的更多資訊請見: Qwen3 TTS 之旅:語音嵌入 本步驟的前置作業請見: Qwen3 TTS 之旅:資料集預處理

運行當下 intel_gpu_top 顯示只有使用一半,另一方面所有 CPU 則是跑滿的狀態且負載皆在伺服器端,但是我並沒有仔細研究性能瓶頸在哪裡,推測是伺服器的 mp3 轉換的影響。 當然也有 JSON 和 base64 的解編碼運算,不過這件事情資料處理端也有,但是並沒有佔用太多 CPU,因此可能性比較低。

因此我花了一點時間實作了運算進度存檔的機制,以 100 筆資料為一個單位,除了將 100 筆作為一個 Transaction 以外,還會在用於紀錄進度的資料表紀錄進度,若過程中中斷,下次運算便可從紀錄點繼續。

向量資料庫

Milvus 是蠻有名氣的向量資料庫實作之一,不過當下我並沒有檢索的需求,僅需要一個儲存的載體,因此選擇了其 Lite 版本,會將資料儲存成 SQLite 格式。

然而實際使用時在運算到 4.9k 的資料左右會出現異常:

Assert "suc"  => failed to parse insert data from records at /workspace/milvus-lite/thirdparty/milvus/internal/core/src/segcore/segment_c.cpp:303

不確定是不是到達 1M~10M 左右的官方上限:

info

嵌入向量是 2048 維,中斷時大約是 4.9k資料;因此: 4900×2048≈10M

所以最後將向量的 JSON 序化成字串後直接使用 SQLite 儲存,雖然效率很差,但是當下最重要的是先把資料做嵌入之後找地方放,而且方便後續步驟提取,應該避免無謂的為了效率而提高程式碼複雜度。

SQLite 雖然是「輕量」的資料庫實作,但是不代表它隨便,專案下的測試程式碼甚至比實作本身還多。並且在摸索資料的階段,可攜性、方便轉移複製版控...與生產環境需要考量的性能、低冗餘與最佳化...屬於不同情境。

dataset

在進行這個主題的額外收穫是發現這個名為 dataset 的 Python 套件,它可以像這樣:

import dataset

db = dataset.connect('sqlite:///:memory:')

table = db['sometable']
table.insert(dict(name='John Doe', age=37))
table.insert(dict(name='Jane Doe', age=34, gender='female'))

john = table.find_one(name='John Doe')

直接操作一個 SQLite 實例讀寫資料,無須預先定義 Schema 或是新增資料表。

Wei Ji

這個文章是「Qwen3 TTS 之旅」系列的一部分,關於旅程的起因與整體概覽請見:

在我開始撰寫主成份分析、視覺化...等程式以前,先對相關的領域知識進行學習,最後定位了「流形學習 (manifold learning)」這個領域,雖然「流形學習」這個詞比較正式且有嚴謹的定義,不過本文比較接近學習筆記。

本文雖然跟學術性的文章比起來隨性很多,但是跟其他軟體實作相比又抽象了許多,所以額外拉出來當成獨立的主題談:

視覺化與降維

一個語音經過 Qwen3 模型嵌入之後,會得到 2048 個數字,這 2048 個數字稍微學術一點會被稱呼為「高維向量」或是「高維空間」,2048 個數字排列組合構成的所有可能性便是「空間」,特定的 2048 個數字,也就是某個資料點就是「向量」。

然而人類並不能直接理解這 2048 個數字代表的意思,也就是人類不能理解高維空間或是高維向量的內含。

理解空間的其中一種方法就是視覺化,但是紙張只能呈現二維的圖像、就算透過軟體與之互動也只能呈現三維的畫面,那麼我們要如何把 2048 個數字變成等效的 3 個數字然後做成圖表呢?是的這個過程就是降維。

「沒吃過豬也看過豬走路」是我的中心思想之一,在進行更進一步的介紹以前我們先隨便看幾個降維的例子吧1

它分別對兩個三維的資料用各種不同的方法降成二維。

流形 (Manifolds)

warning

以下是關於流形這個概念的通俗解釋,不具備數學上的嚴謹性。

「2048 維的高維空間」聽起來好像很厲害,不過我們可以這樣假設:

這個高維空間其實很稀疏

舉例來說,從宏觀角度來看宇宙,會發現它像海綿一樣充滿了空孔2

只有少數的空間有被恆星和物質填充。有興趣的朋友可以聽聽看 YouTube 頻道 Kurzgesagt 的介紹

又或是揉成一團的報紙,即便我們在三維空間觀察它是一個球體,但是它實際上充滿了空氣,真正有資訊的部份是以二維紙張的形式分佈的。

這個實際上是低維情報,但是在高維空間中被觀察到的東西,就是流形。

info

嚴謹的流形定義還包含了「局部近似歐幾里得空間」,只是解釋起來比較麻煩,所以以上描述省略了。 就像地球表面是弧形的,但是放大之後會接近平面。

流形學習則是把揉成球的報紙攤平的過程,並且是一種機器學習。

降維

最簡單的降維方式就是投影:

現實的例子就是你跟你的影子,找一個平面/方向就能把三維的資料壓成二維的,如何找到那個「正確的平面」則是最重要的問題。

然後讓我們回來看這張圖:

可以發現 S 型的資料在 ISOMAP 算法下被攤成漂亮的方形,接著再想想要投影出這個漂亮的方形的「面」長什麼樣子?想必不是一個平面對吧?

學術上會把這兩者分別稱呼成線性跟非線性的降維技術。

流形學習其實就是降維炫泡一點的講法。

PCA

主成份分析 (PCA, Principal component analysis) 簡單來說是一種對資料進行線性降維的技巧,而且屬於非監督機器學習。

OS:恩?怎麼突然之間我就機器學習了!?(◐_◑)

至於不簡單來說嘛...它建立在不少統計與數學工具與概念之上...

敘述統計

把幾個基本的敘述統計概念列一下。

  • 母體平均數 (Mean)
    • μ\mu
  • 標準差
    • σ=1Ni=1N(xiμ)2\sigma ={\sqrt {{\frac {1}{N}}\sum _{i=1}^{N}(x_{i}-\mu )^{2}}}
    • 需要先建立在平均數之上。
  • 變異數 (Variance)
    • Var(X)=σ2\operatorname {Var} (X)=\sigma ^{2}
    • 概念上是標準差的平方,但是實務上通常會先算出變異數,開根號之後才會得到標準差。

共變異數 (Covariance)

cov(X,Y)=1Ni=1N(xiμx)(yiμy)\operatorname {cov}(X, Y) = {{\frac {1}{N}}\sum _{i=1}^{N}(x_{i}-\mu_x )(y_{i}-\mu_y )}

計算兩個隨機變數的關聯性,越接近零代表兩個隨機變數越不相關。

共變異數矩陣(Covariance Matrix)

共變異數矩陣是 pp 個特徵交叉進行獲得共變異數構成的矩陣。以下是五維資料的共變異數矩陣例子3

共變異數矩陣的符號:

  • C=[Cjj]\mathbf {C} =[C_{jj'}]
  • KXiXj=cov[Xi,Xj]\operatorname {K} _{X_{i}X_{j}}=\operatorname {cov} [X_{i},X_{j}]
  • 在一些地方會使用符號 Σ\Sigma 代表。

特徵值與特徵向量

一般的 PCA 介紹大概會提到這個步驟:

對共變異數矩陣求特徵值 (eigenvalue) 與特徵向量 (eigenvector)

但是這個步驟開始比較抽象了,特徵值?特徵向量是啥?為什麼共變異矩陣的特徵值跟特徵向量會跟主成份有關系?

這裡我要試著用工程數學的經隨:先射箭再畫靶,來解釋這件事情。

首先我們假設一個白化空間 (whitening space),每一個維度都是標準常態分佈且每一個維度的隨機變數都獨立於彼此:

在這個 nn 維空間的向量可以透過一個 n×nn\times n矩陣變換到另外一個歪曲的空間去,圓形可能變成橢圓形。

共變異數矩陣反應的就是這個白化空間經過變換矩陣得到的觀察結果,那個歪曲的空間就是我們觀測到的空間,因此求那個矩陣的的特徵值與特徵向量就是分解這個矩陣的變換行為。

特徵向量是原本在這個白化空間中正交的向量經過變換後在觀察空間的表示,而特徵值則是從白化空間變換到觀察空間拉伸的比率。

所以我們可以知道,白化空間中一部分的維度在觀察空間中會被放大,換言之,觀察空間中的主要變化可能來自於白化空間中少數幾個維度的貢獻。

這個白化空間就是我們的主成份空間。(這句話嚴謹意義上不完全正確,但是方便理解)

解釋變異量 (Explained Variance)

解釋變異量是各個特徵值在所有特徵值和的佔比,代表著主成份空間某個維度對觀察空間資訊量貢獻的比例,因為特徵值越大、成份在觀察空間中放大的程度就越大。

將解釋變異量排序後畫成圖表便能一目了然各個主成份的佔比為何,積分後便得到累積解釋變異圖(Cumulative Explained Variance):

綠色方條是解釋變異量;紅色曲線是累積解釋變異。透過累積解釋變異我們可以知道降維到幾個主成份可以保留足夠多的特徵。

實務上可取 70% 左右的固定值或是觀察拐點;即便主成份比例低,但是其他成份分佈過於均勻的話依然可以視為雜訊。

PCA 降維

PCA 的最後一個步驟。有了特徵向量、特徵值並選定主成份維度,便可構造一個矩陣把高維資料進行線性變換以實現降維了。

Footnotes

  1. Principal Component Analysis(PCA) | by venkateshtantravahi | Medium. Retrieved 2026-04-06, from https://vtantravahi.medium.com/principal-component-analysis-pca-37dc2c22cdf0

  2. New Research Supports the Idea That We Live in a Void. Retrieved 2026-04-06, from https://scitechdaily.com/new-research-supports-the-idea-that-we-live-in-a-void/

  3. Principal Component Analysis Made Easy: A Step-by-Step Tutorial | by Marcus Sena | TDS Archive | Medium. Retrieved 2026-04-06, from https://medium.com/data-science/principal-component-analysis-made-easy-a-step-by-step-tutorial-184f295e97fe

Wei Ji

從我踏入 Qwen3 TTS 這個兔子洞到現在也已經兩個多禮拜了 (2026-03-20~2026-04-06),即便還沒徹底完成一開始設定的目標也該設定一個存檔點;把歷程做個紀錄,然後回去忙其他事情了。然而因為整個過程並不是一個線性的故事,很難結構化的描述,因此本文會先概述整個事件的全面以及這個兔子洞是怎麼發生的,細節將由其他文章補充。

火種

這個旅途說是因為一張圖而起的也不違過:

Qwen3-TTS 是 2026 年一月釋出極為先進的 TTS 開放權重模型,而有個老兄將該模型前段一區用於進行語音嵌入的類神經網路抽出做成可獨立運行的模型1,這裡要稍微解釋一下這件事為什麼了不起,從嵌入的角度來說:

  1. OpenAI-Compatible API 中的 /embeddings 端點僅覆蓋了「文字嵌入」,語音嵌入在工程應用領域依然相對稀疏。(細節我會在文章後面的內容詳談)
  2. 它是開箱即用的開放權重模型,不像一些語音嵌入模型僅有論文。
  3. 該模型產出的嵌入向量可作為後端聲音複製 (Voice Clone) 模型的輸入,因此該嵌入向量是有額外工程用途的,而不是單純比較餘弦距離來判斷語音音色的相似度。

從語音的角度:

  1. 基於文字的 TTS 其輸出並不穩定,因為「一個沈重的男子聲音」可以存在無限多種解。
  2. 大部分基於語音複製的 TTS 需要仰賴原音樣本的存在。
  3. 大部分基於語音複製模型須提供語音的內文標籤,往往會鎖定特定語言。(原音樣本鎖定特定語言)

因此只要能調控嵌入空間,就能產生一個穩定的 TTS 客製化流水線。

構想

於是一個構想油然而生:

透過資料降維的技術將一個已知資料集的嵌入向量視覺化,使用者在三維的的空間中可以選定某個資料點試聽,同時也可以也可以選定空間中不存在資料點的空間,透過降維的反函數生成嵌入向量,將開嵌入向量傳遞給 TTS 進行生成。

在三維空間中透過已知資料集的標籤與特徵讓使用者理解某個區域具有某些語音特徵,例如:男性特徵、女性特徵、年長者特徵...。

簡單來說這是一個能夠客製化聲音的 TTS 工作流程。

旅程概覽

整個旅途到目前為止大概可以分成幾個主題:

  • Voice Embedding: 我把玩 Qwen3 Voice Embedding 以及進行一些模組化的過程。
  • Audio ETL (Extract, Transform, Load):對 cv-corpus-25.0-2026-03-09 (Mozilla Common Voice 25.0) 進行一些預處理的過程。
  • Embedding ETL:對資料集進行嵌入的過程。
  • PCA ETL:對嵌入向量的資料集進行主成份分析 (Principal components analysis) 的過程。
  • Qwen3 TTS ONNX:我把玩 Qwen3 TTS ONNX 以及試圖進行一些模組化的過程。
  • Continuity Test:嵌入空間連續性測試。
  • Manifold Learning:對「資料降維」這一領域進行學習的過程。

並且上述主題並沒有明確的嵌後順序,而且事件相互交錯,在後續的文章我會試著僅以一個主題為主軸、相關事件為輔的方式描述,讓整個敘述比較不會太混亂。

Footnotes

  1. Qwen3's most underrated feature: Voice embeddings : r/LocalLLaMA. Retrieved 2026-04-02, from https://www.reddit.com/r/LocalLLaMA/comments/1rc59ze/qwen3s_most_underrated_feature_voice_embeddings/