使用 C++ 進行深度學習的語音辨識:Whisper.cpp

| | 0 Comments| 08:27
Categories:

Whisper 是 OpenAI 在 2022 年推出來的通用語音辨識模型(GitHub)。他基本上支援多語言的偵測、也還有內建翻譯成英文的功能,基本上算是一個相當好的語音辨識框架。

而由於目前深度學習領域主流的開發環境還是 Python,所以不意外地,OpenAI 提供的開發環境也是以 Python 為主。

不過,目前 Georgi Gerganov 也有開發出 C API 的 whisper.cpp 專案(GitHub),它提供了可以在不需要額外的函示庫的狀況下使用的 whisper API,對於 C / C++ 的開發者來說,應該是相當地友善的~(注意,他只支援推論、不支援訓練)

這篇文章就先不管 Whisper 的原理和運作方式,單純就以 C++ 程式開發者為出發點,來記錄一下要怎麼透過 whisper.cpp 來撰寫一個語音辨識的程式吧!


建置 whisper.cpp

由於他標榜著無相依性、所以要建置 whisper.cpp 在最基本的狀況下、算是相當地簡單的!

在最單純的狀況下,只要把開發環境準備好(在 Windows 下要安裝 Visual Studio 和 CMake),只要把專案 clone 下來,執行下面的指令就可以完成建置了!

mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX:PATH="c:/whispercpp" ..
cmake --build . --config Release
cmake --install .

而建置完之後,必要的檔案(h、lib、dll)則會放在 c:\whispercpp 這個資料夾下。

不過要注意的是,這樣建置的方法是最基本的建置,其實會少很多東西!

比如說在範例的部分會只剩下「main」這個範例,如果要其他範例還會需要 SDL 這個函式庫才行(比如說要存取電腦的麥克風)。

而在計算的部分,也會是最基本的純 CPU 計算,沒有用到 GPU 加速之類的。如果想要透過 GPU 來加速的話,則就需要使用第三方函示庫了~

他有支援 Intel OpenVINO、NVIDIA CUDA、CLBlast 之類的框架;而如果只是要使用 CPU 來運作,透過 OpenBLAS 似乎也是可以做到一定程度的加速。如果要使用這些函示庫來加速的話,在透過 CMake 進行組態設定的時候,也都需要加上特定的參數,不過在這邊就先不提了。


模型

whisper.cpp 支援的模型是量化成整數的 ggml 格式模型。這部分他有提供工具可以轉換,不過要簡單的話,就是先到 Hugging Face 上去下載已經轉好的了~

他的頁面是 https://huggingface.co/ggerganov/whisper.cpp/tree/main,裡面有不同大小、不同版本的檔案,可以根據自己的需要來選擇。

下面是官方提供的使用記憶體的需求表:

Model
磁碟空間
記憶體空間
tiny
75 MiB
~273 MB
base
142 MiB
~388 MB
small
466 MiB
~852 MB
medium
1.5 GiB
~2.1 GB
large
2.9 GiB
~3.9 GB

不過,基本上模型越大效果應該還是會越好,但是相對地所需要的資源就更多。所以要怎麼取捨,就是看自己的需求了。

這邊接下來基本上是拿「ggml-medium.bin」這個檔案來作範例。


使用範例測試

都準備好了之後,接下來就是先測試自己建置出來的程式能不能正確運作吧。

理論上,在 c:\whispercpp\bin 下,會有 whisper.dllmain.exe 這兩個檔案,其中 main.exe 就是官方提供的範例。

要執行範例,最簡單的指令是:

main.exe -m c:\whisperpp\ggml-medium.bin d:\whisper.cpp\samples\jfk.wav

這邊就是要指定前面下載好的模型檔、以及拿一個 .wav 檔案來做輸入的音源了;而這邊使用的檔案,是官方提供的 jfk.wav

執行結果大致上會輸出下面的內容:

whisper_init_from_file_with_params_no_state: loading model from
'c:\whisperpp\ggml-medium.bin' whisper_model_load: loading model whisper_model_load: n_vocab = 51865 whisper_model_load: n_audio_ctx = 1500 whisper_model_load: n_audio_state = 1024 whisper_model_load: n_audio_head = 16 whisper_model_load: n_audio_layer = 24 whisper_model_load: n_text_ctx = 448 whisper_model_load: n_text_state = 1024 whisper_model_load: n_text_head = 16 whisper_model_load: n_text_layer = 24 whisper_model_load: n_mels = 80 whisper_model_load: ftype = 1 whisper_model_load: qntvr = 0 whisper_model_load: type = 4 (medium) whisper_model_load: adding 1608 extra tokens whisper_model_load: n_langs = 99 ggml_init_cublas: GGML_CUDA_FORCE_MMQ: no ggml_init_cublas: CUDA_USE_TENSOR_CORES: yes ggml_init_cublas: found 1 CUDA devices: Device 0: NVIDIA GeForce RTX 4090, compute capability 8.9, VMM: yes whisper_backend_init: using CUDA backend whisper_model_load: CUDA0 total size = 1533.52 MB (2 buffers) whisper_model_load: model size = 1533.14 MB whisper_backend_init: using CUDA backend whisper_init_state: kv self size = 132.12 MB whisper_init_state: kv cross size = 147.46 MB whisper_init_state: compute buffer (conv) = 25.61 MB whisper_init_state: compute buffer (encode) = 170.28 MB whisper_init_state: compute buffer (cross) = 7.85 MB whisper_init_state: compute buffer (decode) = 98.32 MB system_info: n_threads = 4 / 32 | AVX = 1 | AVX2 = 1 | AVX512 = 0
| FMA = 1 | NEON = 0 | ARM_FMA = 0 | METAL = 0 | F16C = 1
| FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 1 | SSSE3 = 0
| VSX = 0 | CUDA = 1 | COREML = 0 | OPENVINO = 0 | main: processing '..\..\samples\jfk.wav' (176000 samples, 11.0 sec),
4 threads, 1 processors, 5 beams + best of 5, lang = en,
task = transcribe, timestamps = 1 ... [00:00:00.000 --> 00:00:11.000] And so my fellow Americans,
ask not what your country can do for you,
ask what you can do for your country.
whisper_print_timings: load time = 1053.01 ms whisper_print_timings: fallbacks = 0 p / 0 h whisper_print_timings: mel time = 6.69 ms whisper_print_timings: sample time = 68.48 ms / 143 runs ( 0.48 ms per run) whisper_print_timings: encode time = 73.72 ms / 1 runs (73.72 ms per run) whisper_print_timings: decode time = 0.00 ms / 1 runs ( 0.00 ms per run) whisper_print_timings: batchd time = 456.64 ms / 141 runs ( 3.24 ms per run) whisper_print_timings: prompt time = 0.00 ms / 1 runs ( 0.00 ms per run) whisper_print_timings: total time = 1665.32 ms

其中,黃字的部分就是辨識的結果(本來只有一行)了~

實際上,Heresy 這邊在建置的時候其實有加上 NVIDIA CUDA,所以速度會快很多、而且顯示出來的資訊也會不太一樣。

另外,如果想要玩別的聲音檔案的話,由於目前這個範例只支援採樣頻率是 16k 的單聲道 WAV,所以會需要先轉換成這樣的格式才能給它處理。如果是使用 ffmpeg 轉換的話,指令如下:

ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav

理論上這樣轉換後的 WAV 檔是可以支援的。


C API 基本範例

雖然 whisper.cpp 的名字有 C++,但是實際上他提供的是 C 風格的 API。

而他最基本的使用形式,大概會是像下面的樣子:

#include "whisper.h"
 
#include <vector>
#include <iostream>
 
int main()
{
  whisper_context_params cparams = whisper_context_default_params();
  whisper_context* ctx = whisper_init_from_file_with_params(
"/path/to/ggml-base.bin", cparams); std::vector<float> pcmf32; // get audio whisper_full_params wparams = whisper_full_default_params(WHISPER_SAMPLING_GREEDY); if (whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size()) == 0) { const int n_segments = whisper_full_n_segments(ctx); for (int i = 0; i < n_segments; ++i) { const char* text = whisper_full_get_segment_text(ctx, i); std::cout << text << "\n"; } } whisper_free(ctx); }

不過要注意,這段程式碼沒有真正的聲音資訊(pcmf32),所以實際上是不會真的有東西輸出的。

context

基本上,要使用的時候要先建立一個對應到模型的 context,他的型別是 whisper_context,最簡單的方法就是透過 whisper_init_from_file_with_params() 這個函式來讀取模型並建立 context。

除了給一個路徑讓他從檔案去讀取外,他也可以直接去使用已經讀到記憶體裡面的資料、或是自定義讀取方式來做初始化。

而在初始化的時候,還要指定 whisper_context_params 這個參數;在目前的版本它的內容很簡單,就是要不要使用 GPU 而已。

在不需要使用的時候,則是要透過 whisper_free() 來釋放記憶體。

推論

初始化完成之後,接下來就可以進行推論(inference)、辨識輸入的語音了。

基本上,它內部的處理流程會分成好幾的階段,但是對於一般只希望取得最後的辨識結果的人還說,只需要使用 whisper_full() 這個函式就可以了!

他的第一個參數是 context、就是前面初始化好的 whisper_context 的指標。

第二個參數則是要用來推論的設定、格式是 whisper_full_params,裡面很複雜,能調的東西很多,全部用預設值基本上就是拿來測試的方法了。

第三個參數則是一個 const float*、代表要拿來辨識的聲音資訊;第四個參數則是他的大小。

而如果這個函式回傳 0,就是推論成功了!

讀取結果

whisper.cpp 在推論完之後,結果會分成好一個或多個 segment、每個 segment 裡面會有很多 token。

一個 segment 可能就幾個字、也有可能是句子或段落;而 token 則是他在處理時的最小單位、但是基於設計的原理,token 並不是對應到字元、單字這類的東西,以中文字來說,一個中文字甚至可能會被拆成兩個 token。

而要讀取結果,基本上就是先透過 whisper_full_n_segments() 來取得推論結果的 segment 的數量,這邊是 n_segments

之後,則就可以透過 whisper_full_get_segment_text() 來把想要的 segment 代表的文字讀取出來了。

而如果想要這段文字的時間點的話,則可以透過 whisper_full_get_segment_t0()whisper_full_get_segment_t1() 這兩個函式來取得起始、結束的時間。

基本上,最簡單的使用方法大概就是這樣了~
某方面來說,比較麻煩的可能反而是要處理音訊資料了。 XD


處理 token

如同前面所提到的,whisper 最小的處理單元是 token、而上面用來得到文字的 segment 基本上也是 token 的組成。所以如果有必要的話,也是可以針對 token 來做操作,取得更進一步的細部資訊的。

這邊的程式可以寫成:

const int n_segments = whisper_full_n_segments(ctx);
for (int i = 0; i < n_segments; ++i)
{
  const int n_tokens = whisper_full_n_tokens(ctx, i);
  for (int t = 0; t < n_tokens; ++t)
  {
    const char* text = whisper_full_get_token_text(ctx, i, t);
    float p = whisper_full_get_token_p(ctx, i, t);
  }
}

基本上,就是先透過 whisper_full_n_tokens() 來取得第 i 個 segment 裡面有幾個 token,然後再透過迴圈去讀取個別 token 的資訊了。

這邊是透過 whisper_full_get_token_text() 來讀取 token 的文字、而 whisper_full_get_token_p() 則是用來讀取他的可靠性。

如果要更細節的資料,也可以透過 whisper_full_get_token_data() 來取得 whisper_token_data 這個結構的資料,裡面會包含時間資訊、更細節的機率等資訊。

不過如果是要辨識中文或其他非英文的語言的話,在輸出文字的時候應該不適合用 token 做輸出;因為這邊得到的資料可能不是代表完整的中文字,直接輸出會出問題。

而如果以官方的 main 這個範例說,如果加上「-pc」這個參數後,他就會將每個 token 都根據可能性加上顏色,有興趣的話可以自己玩看看。


基本參數

前面有提到,要推論的時候除了聲音的資料外,還要給他一個 whisper_full_params 來作為推論時的設定參數。

而這個結構裡面定義的東西非常多,很多東西大概也得自己去測試、慢慢調整才會知道怎樣設定比較合適了。

不過,這邊先列一些比較基本的參數出來講一下:

  • n_threads:要使用幾個執行序來進行推論
  • language:預設的語言,可以給 nullptr"""auto" 讓他自動偵測,或是給 "en" / "english" 代表英文、"zh" / "chinese" 代表中文。
  • translate:設定成 true 的話,他會把結果翻譯成英文。

此外,他也可以設定提示詞(prompt)或 callback 這類的東西,不過這邊就先不講了。


這篇大概就這樣了。

基本上,whisper.cpp 基本使用比想像的簡單不少,花比較多時間的反而是在研究怎麼透過 Qt6 讀取麥克風的資料(苦笑),這部分之後有機會在整理了。

不過老實說,真正的問題是在抓到文字之後到底要怎麼做後續的處理了?如果要拿來做簡單的單字比對問題應該不大,但是如果要能做語意分析的話,頭就會很大了。(眼神死

而這個作者另外還有一個 llama.cpp 的專案(GitHub),是讓開發者可以透過 C API 來使用大語言模型(例如 ChatGPT)的,或許之後可以考慮串起來吧~
(其實 whisper.cpp 也有一個範例 talk-llama 是串好的)


附註:

使用 main 這個範例辨識中文語音內容的時候,預設會自動翻譯成英文,如果要保留中文輸出,需要加上 -l zh 這個參數、指定內容是中文。

不過由於 PowerShell 預設編碼不是 UTF-8,所以可能會看到輸出一堆看不懂的亂碼。這時候建議先執行下面的指令:

[System.Console]::OutputEncoding = [System.Console]::InputEncoding = 
[System.Text.Encoding]::UTF8

額外參考:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *