1. 程式人生 > >Kaldi學習手記(三): Kaldi 的I/O機制

Kaldi學習手記(三): Kaldi 的I/O機制

一、命令列級 I/O 機制

命令列 I/O 是指在 shell 上呼叫編譯好的 Kaldi 工具的方法。比如,如果想檢視音訊檔案的時長,就可以使用這樣的命令
wav-to-duration scp:wav.scp ark,t:-
其中,wav-to-duration 是 Kaldi 中的檢視時長的工具,後面兩個是引數。下面具體介紹命令列級 Kaldi 的 I/O。

非表格型I/O

非表格型的 I/O 一般比較簡單,一般是輸入輸出只包含一個或兩個物件的檔案或流(比如,聲學模型檔案,變換矩陣)。使用方式就是 shell 命令的使用方式。其中值得注意的有:
- Kaldi 採用一種擴充套件的檔名進行輸入輸出。讀取檔案的檔名稱為 rxfilename , 寫入的檔名稱為 wxfilename。
- 在 rxfilename/wxfilename 處採用 “-” 來表示標準輸入輸出。
- rxfilename/wxfilename 處可以使用管道命令,比如先用gunzip將壓縮檔案解壓,再輸入到 Kaldi 程式中,即可在檔案輸入路徑處填入 “gunzip -c foo.gz|”。
- rxfilename/wxfilename 後可以通過 “:” 來描述偏移量,如 “foo:1045” 表示從 foo 檔案偏移 1045 個位元組開始讀取。
- 使用 –binary=true/false 來控制是否使用二進位制輸出。預設是true。
- 有一組 copy* 命令,可以用來檢視檔案。如,copy-matrix –binary=false foo.mat -。
- Log 資訊和輸出會混在一起顯示,但是 Log 資訊是 stderr 上,所以不會傳到管道中。可以通過新增 2>/dev/null 命令,將log資訊重定向到 /dev/null。

下面列舉一些例子:
echo ‘[ 0 1 ]’ | copy-matrix –binary=false - -
將矩陣 [ 0 1 ] 輸入到 copy-matrix 中,再顯示出來。亦可以將其表示為
copy-matrix –binary=false ‘echo [ 0 1 ]|’ -

表格型I/O

對於一系列資料的集合,比如對應於每一句話的特徵矩陣,或者每一條音訊檔案對應的路徑,Kaldi 採用表格形式的資料表示。表格中,以沒有空格的字串為索引。Kaldi 中,稱從表格檔案中讀取的一個字串為 rspecifier, 寫入表格檔案的一個字串為wspecifier。有兩種表格檔案格式:archive 和 script 。其讀取方式和非表格型資料類似,也是使用 rxfilename/wxfilename的格式,其格式為[options]:path:[offset]。需要在 options 中註明是archive檔案還是script檔案。例如
wav-to-duration scp:wav.scp ark,t:-

rspecifiers 和 wspecifier

rspecifiers 和 wspecifier 是 Kaldi 中定義的兩種術語,是存在於表格檔案中的檔名,格式類似於 rxfilename/wxfilename, 亦可以採用管道命令呢,對於ark檔案,可以新增選項,如ark,s,cs:-等。

兩種儲存格式:script-file 和 archive-file

script-file 是一種檔案指標,裡面包含的是具體檔案所在路徑,其格式為
key rspecifier|wspecifier
key 是欄位名字,rspecifier/wspecifier則是檔案路徑,rspecifier/wspecifier 可以有空格。例如 “utt1 gunzip -c /foo/bar/utt1.mat.gz”。
archive-file 裡面裝的是實際的資料。其檔案格式是
key1 object1 key2 object2 key3 object3 …
ark檔案可以連線在一起,依然是一個有效的ark檔案。但是如果需要的檔案時有序的話,連線的時候應該要注意。
ark檔案和scp檔案有可能同時寫,這時可以這麼寫:ark,scp:foo.ark,foo.scp。

二、程式碼級 I/O 機制

典型的讀寫過程如下:

{ // input.
  bool binary_in;
  Input ki(some_rxfilename, &binary_in);
  my_object.Read(ki.Stream(), binary_in);
  // you can have more than one object in a file:
  my_other_object.Read(ki.Stream(), binary_in);
}
// output.  note, "binary" is probably a command-line option.
{
  Output ko(some_wxfilename, binary);
  my_object.Write(ko.Stream(), binary); 
}

花括號是用來限制輸入輸出物件的作用域的,這樣可以自動析構輸入輸出物件,自動關閉檔案。my_object是需要從檔案讀取到記憶體的物件。

檔案讀寫

檔案的讀寫採用 Input/Output 類
Input 類的介面有:
- 建構函式:Input (const std::string &rxfilename, bool *contents_binary=NULL) 和 Input ()
- bool Open (const std::string &rxfilename, bool *contents_binary=NULL)
- bool OpenTextMode (const std::string &rxfilename)
- bool IsOpen ()
- int32 Close ()
- std::istream & Stream ()
- 解構函式~Input ()
可以顯式地呼叫Open()或Close() 來開啟或關閉檔案。Stream () 來讀取檔案流。
類似地,Output 類的介面有:
- 建構函式 Output (const std::string &filename, bool binary, bool write_header=true)和Output ()
- bool Open (const std::string &wxfilename, bool binary, bool write_header)
- bool IsOpen ()
- std::ostream & Stream ()
- bool Close ()
- ~Output ()

物件(Object)讀寫

Kaldi 的類中一般都有讀寫藉口,其形式為

class SomeKaldiClass {
 public:
   void Read(std::istream &is, bool binary);
   void Write(std::ostream &os, bool binary) const;
};

一般會通過摸板函式ReadKaldiObject(const std::string &filename, C *c) 和 WriteKaldiObject(const C &c, const std::string &filename, bool binary) 來呼叫某個類的Read() 和 Write ()。

表格(Table)讀寫

對於表格檔案(scp,ark),Kaldi 定義了專門的表格讀寫器。表格讀寫器是關於 Holder 類的模板類。Holder 是一些輔助類,負責告訴表格讀寫器,物件的型別是什麼,應該如何讀寫。表格讀寫器有四個類:
- RandomAccessTableReader
- SequentialTableReader
- TableWriter
- RandomAccessTableReaderMapped
這裡面,Sequential 表示序列化訪問,即支援使用迭代器遍歷; Random 表示隨機訪問某幾個。
讀寫的時候,需要提供 wspecifiers/rspecifiers 給讀寫器。
例子:

std::string feature_rspecifier = "scp:/tmp/my_orig_features.scp",
   transform_rspecifier = "ark:/tmp/transforms.ark",
   feature_wspecifier = "ark,t:/tmp/new_features.ark";
// there are actually more convenient typedefs for the types below,
// e.g. BaseFloatMatrixWriter, SequentialBaseFloatMatrixReader, etc.
TableWriter<BaseFloatMatrixHolder> feature_writer(feature_wspecifier);
SequentialTableReader<BaseFloatMatrixHolder> feature_reader(feature_rspecifier);
RandomAccessTableReader<BaseFloatMatrixHolder> transform_reader(transform_rspecifier);
for(; !feature_reader.Done(); feature_reader.Next()) {
   std::string utt = feature_reader.Key();
   if(transform_reader.HasKey(utt)) {
      Matrix<BaseFloat> new_feats(feature_reader.Value());
      ApplyFmllrTransform(new_feats, transform_reader.Value(utt));
      feature_writer.Write(utt, new_feats);
   }
}

對於表格,Kaldi 將其看為一系列名值對。一般除了前面介紹的Open() 等成員函式外,還有這幾個函式:
- 對 RandomAccessTableReader 類:
const T &Value(const std::string &key) 返回對應於 key 的鍵值的引用。
bool HasKey(const std::string &key) 表明是否有這個key。
- 對 SequentialTableReader 類:
inline std::string Key() 返回對當前鍵 key 的引用
const T &Value() 返回對應於當前鍵 key 的鍵值的引用
void Next() 迭代到下一個鍵。
inline bool Done() 表明是否完成迭代。
void FreeCurrent() 在使用大物件時,用來節約記憶體。
- 對 SequentialTableReader 類:
inline void Write(const std::string &key, const T &value) 表示寫入鍵值對。
void Flush() 為重新整理緩衝區。
- 對 RandomAccessTableReaderMapped 類:
這個類是用來隨機訪問每一個句子對應一個key,有很多相同的說話人的情況。如rxfilename檔案中有這樣的行
utt1 spk1
utt2 spk1
utt3 spk1
它在使用時,除了需要提供rxfilename, 還需要提供utt2spk的對映表
RandomAccessTableReaderMapped(const std::string table_rxfilename, const std::string &utt2spk_rxfilename)

附錄

1.rspecifier 的選項

Options Meanings
“o” 斷言每一個條目只讀取一次
“p” 無視資料錯誤,繼續下一條
“s” key按照排序後的字串讀取
“cs” 要求key排序
“no” “o”的反義詞
“np” “p”的反義詞
“ns” “s”的反義詞
“ncs” “cs”的反義詞
“b” 無作用
“t” 無作用

有關程式碼檔案:
kaldi-io.h