天天看點

解析WeNet雲端推理部署代碼

摘要:WeNet是一款開源端到端ASR工具包,它與ESPnet等開源語音項目相比,最大的優勢在于提供了從訓練到部署的一整套工具鍊,使ASR服務的工業落地更加簡單。

本文分享自華為雲社群《WeNet雲端推理部署代碼解析》,作者:xiaoye0829 。

WeNet是一款開源端到端ASR工具包,它與ESPnet等開源語音項目相比,最大的優勢在于提供了從訓練到部署的一整套工具鍊,使ASR服務的工業落地更加簡單。如圖1所示,WeNet工具包完全依賴于PyTorch生态:使用TorchScript進行模型開發,使用Torchaudio進行動态特征提取,使用DistributedDataParallel進行分布式訓練,使用torch JIT(Just In Time)進行模型導出,使用LibTorch作為生産環境運作時。本系列将對WeNet雲端推理部署代碼進行解析。

解析WeNet雲端推理部署代碼

圖1:WeNet系統設計[1]

WeNet雲端推理和部署代碼位于wenet/runtime/server/x86路徑下,程式設計語言為C++,其結構如下所示:

解析WeNet雲端推理部署代碼

其中:

語音檔案讀入與特征提取相關代碼位于frontend檔案夾下;

端到端模型導入、端點檢測與語音解碼識别相關代碼位于decoder檔案夾下,WeNet支援CTC prefix beam search和融合了WFST的CTC beam search這兩種解碼算法,後者的實作大量借鑒了Kaldi,相關代碼放在kaldi檔案夾下;

在服務化方面,WeNet分别實作了基于WebSocket和基于gRPC的兩套服務端與用戶端,基于WebSocket的實作位于websocket檔案夾下,基于gRPC的實作位于grpc檔案夾下,兩種實作的入口main函數代碼都位于bin檔案夾下。

日志、計時、字元串處理等輔助代碼位于utils檔案夾下。

WeNet提供了CMakeLists.txt和Dockerfile,使得使用者能友善地進行項目編譯和鏡像建構。

WeNet隻支援44位元組header的wav格式音頻資料,wav header定義在WavHeader結構體中,包括音頻格式、聲道數、采樣率等音頻元資訊。WavReader類用于語音檔案讀入,調用fopen打開語音檔案後,WavReader先讀入WavHeader大小的資料(也就是44位元組),再根據WavHeader中的元資訊确定待讀入音頻資料的大小,最後調用fread把音頻資料讀入buffer,并通過static_cast把資料轉化為float類型。

這裡存在的一個風險是,如果WavHeader中存放的元資訊有誤,則會影響到語音資料的正确讀入。

WeNet使用的特征是fbank,通過FeaturePipelineConfig結構體進行特征設定。預設幀長為25ms,幀移為10ms,采樣率和fbank維數則由使用者輸入。

用于特征提取的類是FeaturePipeline。為了同時支援流式與非流式語音識别,FeaturePipeline類中設定了input_finished_屬性來标志輸入是否結束,并通過set_input_finished()成員函數來對input_finished_屬性進行操作。

提取出來的fbank特征放在feature_queue_中,feature_queue_的類型是BlockingQueue<std::vector<float>>。BlockingQueue類是WeNet實作的一個阻塞隊列,初始化的時候需要提供隊列的容量(capacity),通過Push()函數向隊列中增加特征,通過Pop()函數從隊列中讀取特征:

當feature_queue_中的feature數量超過capacity,則Push線程被挂起,等待feature_queue_.Pop()釋放出空間。

當feature_queue_為空,則Pop線程被挂起,等待feature_queue_.Push()。

線程的挂起和恢複是通過C++标準庫中的線程同步原語std::mutex、std::condition_variable等實作。

線程同步還用在AcceptWaveform和ReadOne兩個成員函數中,AcceptWaveform把語音資料提取得到的fbank特征放到feature_queue_中,ReadOne成員函數則把特征從feature_queue_中讀出,是經典的生産者消費者模式。

通過torch::jit::load對存在磁盤上的模型進行反序列化,得到一個ScriptModule對象。

WeNet推理支援的解碼方式都繼承自基類SearchInterface,如果要新增解碼算法,則需繼承SearchInterface類,并提供該類中所有純虛函數的實作,包括:

目前WeNet隻提供了SearchInterface的兩種子類實作,也即兩種解碼算法,分别定義在CtcPrefixBeamSearch和CtcWfstBeamSearch兩個類中。

WeNet支援語音端點檢測,提供了一種基于規則的實作方式,使用者可以通過CtcEndpointConfig結構體和CtcEndpointRule結構體進行規則配置。WeNet預設的規則有三條:

檢測到了5s的靜音,則認為檢測到端點;

解碼出了任意時長的語音後,檢測到了1s的靜音,則認為檢測到端點;

解碼出了20s的語音,則認為檢測到端點。

一旦檢測到端點,則結束解碼。另外,WeNet把解碼得到的空白符(blank)視作靜音。

WeNet提供的解碼器定義在TorchAsrDecoder類中。如圖3所示,WeNet支援雙向解碼,即疊加從左往右解碼和從右往左解碼的結果。在CTC beam search之後,使用者還可以選擇進行attention重打分。

解析WeNet雲端推理部署代碼

圖2:WeNet解碼計算流程[2]

可以通過DecodeOptions結構體進行解碼參數配置,包括如下參數:

其中,ctc_weight表示CTC解碼權重,rescoring_weight表示重打分權重,reverse_weight表示從右往左解碼權重。最終解碼打分的計算方式為:

TorchAsrDecoder對外提供的解碼接口是Decode(),重打分接口是Rescoring()。Decode()傳回的是枚舉類型DecodeState,包括三個枚舉常量:kEndBatch,kEndpoint和kEndFeats,分别表示目前批資料解碼結束、檢測到端點、所有特征解碼結束。

為了支援長語音識别,WeNet還提供了連續解碼接口ResetContinuousDecoding(),它與解碼器重置接口Reset()的差別在于:連續解碼接口會記錄全局已經解碼的語音幀數,并保留目前feature_pipeline_的狀态。

由于流式ASR服務需要在用戶端和服務端之間進行雙向的流式資料傳輸,WeNet實作了兩種支援雙向流式通信的服務化接口,分别基于WebSocket和gRPC。

WebSocket是基于TCP的一種新的網絡協定,與HTTP協定不同,WebSocket允許伺服器主動發送資訊給用戶端。 在連接配接建立後,用戶端和服務端可以連續互相發送資料,而無需在每次發送資料時重新發起連接配接請求。是以大大減小了網絡帶寬的資源消耗 ,在性能上更有優勢。

WebSocket支援文本和二進制兩種格式的資料傳輸 。

WeNet使用了boost庫的WebSocket實作,定義了WebSocketClient(用戶端)和WebSocketServer(服務端)兩個類。

在流式ASR過程中,WebSocketClient給WebSocketServer發送資料可以分為三個步驟:1)發送開始信号與解碼配置;2)發送二進制語音資料:pcm位元組流;3)發送停止信号。從WebSocketClient::SendStartSignal()和WebSocketClient::SendEndSignal()可以看到,開始信号、解碼配置和停止信号都是包裝在json字元串中,通過WebSocket文本格式傳輸。pcm位元組流則通過WebSocket二進制格式進行傳輸。

WebSocketServer在收到資料後,需要先判斷收到的資料是文本還是二進制格式:如果是文本資料,則進行json解析,并根據解析結果進行解碼配置、啟動或停止,處理邏輯定義在ConnectionHandler::OnText()函數中。如果是二進制資料,則進行語音識别,處理邏輯定義在ConnectionHandler::OnSpeechData()中。

WebSocket需要開發者在WebSocketClient和WebSocketServer寫好對應的消息構造和解析代碼,容易出錯。另外,從以上代碼來看,服務需要借助json格式來序列化和反序列化資料,效率沒有protobuf格式高。

對于這些缺點,gRPC架構提供了更好的解決方法。

gRPC是谷歌推出的開源RPC架構,使用HTTP2作為網絡傳輸協定,并使用protobuf作為資料交換格式,有更高的資料傳輸效率。在gRPC架構下,開發者隻需通過一個.proto檔案定義好RPC服務(service)與消息(message),便可通過gRPC提供的代碼生成工具(protoc compiler)自動生成消息構造和解析代碼,使開發者能更好地聚焦于接口設計本身。

進行RPC調用時,gRPC Stub(用戶端)向gRPC Server(服務端)發送.proto檔案中定義的Request消息,gRPC Server在處理完請求之後,通過.proto檔案中定義的Response消息将結果傳回給gRPC Stub。

gRPC具有跨語言特性,支援不同語言寫的微服務進行互動,比如說服務端用C++實作,用戶端用Ruby實作。protoc compiler支援12種語言的代碼生成。

解析WeNet雲端推理部署代碼

圖1:gRPC Server和gRPC Stub互動[1]

WeNet定義的服務為ASR,包含一個Recognize方法,該方法的輸入(Request)、輸出(Response)都是流式資料(stream)。在使用protoc compiler編譯proto檔案後,會得到4個檔案:wenet.grpc.pb.h,http://wenet.grpc.pb.cc,wenet.pb.h,http://wenet.pb.cc。其中,wenet.pb.h/cc中存儲了protobuf資料格式的定義,wenet.grpc.pb.h中存儲了gRPC服務端/用戶端的定義。通過在代碼中包括wenet.pb.h和wenet.grpc.pb.h兩個頭檔案,開發者可以直接使用Request消息和Response消息類,通路其字段。

WeNet gRPC服務端定義了GrpcServer類,該類繼承自wenet.grpc.pb.h中的純虛基類ASR::Service。

語音識别的入口函數是GrpcServer::Recognize,該函數初始化一個GRPCConnectionHandler執行個體來進行語音識别,并通過ServerReaderWriter類的stream對象來傳遞輸入輸出。

WeNet gRPC用戶端定義了GrpcClient類。用戶端在建立與服務端的連接配接時需執行個體化ASR::Stub,并通過ClientReaderWriter類的stream對象,實作雙向流式通信。

http://grpc_client_main.cc中,用戶端分段傳輸語音資料,每0.5s進行一次傳輸,即對于一個采樣率為8k的語音檔案來說,每次傳4000幀資料。為了減小傳輸資料的大小,提升資料傳輸速度,先在用戶端将float類型轉為int16_t,服務端在接受到資料後,再将int16_t轉為float。c++中float為32位。

本文主要對WeNet雲端部署代碼進行解析,介紹了WeNet基于WebSocket和基于gRPC的兩種服務化接口。

WeNet代碼結構清晰,簡潔易用,為語音識别提供了從訓練到部署的一套端到端解決方案,大大促進了工業落地效率,是非常值得借鑒學習的語音開源項目。

[1] https://grpc.io/docs/what-is-grpc/introduction/

[2]WeNet: Production First and Production Ready End-to-End Speech Recognition Toolkit

[3]WeNet源碼

[4]WeNet: Production First and Production Ready End-to-End Speech Recognition Toolkit

[5] U2++: Unified Two-pass Bidirectional End-to-end Model for Speech Recognition

點選關注,第一時間了解華為雲新鮮技術~

繼續閱讀