1. 程式人生 > >【學點Kaldi】Kaldi的I/O機制

【學點Kaldi】Kaldi的I/O機制

本篇給出了Kalid中輸入輸出機制的概覽。(主要參考Kaldi的Doc)

Kaldi中類的輸入輸出介面

Kaldi中定義的類有一個通用的I/O介面。標準的介面如下:

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

注意這兩個成員函式返回的是void型別,不能接一連串的istream或者ostreambinary引數是一個標誌位,表明要讀寫的是binary

資料還是text資料。

Kaldi中的物件是怎麼儲存在檔案中的

上面我們提到,Kaldi進行讀操作的程式碼需要知道用哪種模式(binary modetext mode)。其實我們也不需要準確地追溯一個檔案到底是binary的還是text的。Kaldi物件的檔案提供給我們怎麼取辨識。一個binary的Kaldi檔案以字串"\0B"開頭,而text檔案不需要檔案頭(header)。

拓展檔名:rxfilenames 和 wxfilenames

rxfilenamewxfilename 這兩個詞表示的不是類型別。它們只是一些經常出現在變數名字中的修飾符,它們的含義如下:

  • 一個rxfilename
    是一個字串,作為讀操作的拓展名被Kaldi Input類所解釋。
  • 一個wxfilename是一個字串,作為寫操作的拓展名被Kaldi Output類所解釋。
{ // 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); }

rxfilename的型別如下:

  • “-” 或者 “” 表示標準輸入。
  • “some command |”表示一個輸入管道命令,也就是說,我們可以通過popen()|之前的字串給shell。
  • "/some/filename:12345"表示檔案的偏置,即,我們開啟檔案並定位到位置 12345.
  • "/some/filename"...匹配不到以上模式的檔名都被當做普通的檔名。

wxfilename的型別如下:

  • “-” 或者 “” 表示標準輸入。
  • | some command 表示一個輸出管道命令,也就是說,我們可以通過popen()|之後的字串給shell。
  • "/some/filename"...匹配不到以上模式的檔名都被當做普通的檔名。

表的概念

Kaldi中表(Table)只是一種概念,而非實際的C++類。表由一個已知型別的物件集合而成,這些物件是通過字串(strings)索引的。這些字串必須是tokens(令牌),即沒有空格的非空字串。表的典型例子有:

  • 很多被utterance id索引的特徵檔案(被表示為Matrix<float>)。
  • 很多被utterance id索引的文字標註檔案(transcriptions)(被表示為std::vector<int32>)。
  • 很多被speaker id索引的constrained MLLR轉換(被表示為Matrix<float>)。
    一張表可以以兩種可能的形式存在硬碟中或者pipe中: script檔案或者archive檔案

表的訪問

Kaldi中表可以通過三種方式訪問:TableWriterSequentialTableReaderRandomAccessReader
這些都是模板,但是不是基於表所含的物件,二是基於Holder型別。Holder告訴訪問表的程式碼怎麼取讀寫表所包含的物件,它不是一個實際的類或者基類,而是描述了一系列以Holder結尾的類,比如說,TokenHolderKaldiObjectHolder
Holder “held”的類的型別是typedef Holder::T,其中的Holder是實際所用的Holder類的名字。
為了開啟一個Table型別,我們必須提供一個叫做wspecifier或者rspecifier的字串,告訴表訪問程式碼表示怎麼在硬碟中儲存的以及其他一些指令。我們來看一下這個例子,這段程式碼讀取特徵,經過線性轉換,然後再寫到硬碟上去。

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);
   }
}

比較好的是,這種設定使得程式碼能夠像訪問一般的map或者list一樣訪問表。資料的格式以及讀資料過程的其他方面都能夠由rspecifier或者wspecifer來控制,而無需由呼叫程式碼來處理。在以上的這個例子中,",t" 表示以text的形式寫資料。
當然,理想情況是我們能夠以string-object的方式訪問表(就像map),然而,只要我們不是隨機訪問一個特定的表,一個表中有重複的項是可以的(對於寫操作和順序訪問操作,這些過程中表表現得更像a list of pairs)。

Kaldi script檔案格式:

script檔案是一種文字檔案,每一行長得像這樣:

some_string_identifier   /some/filename

另外一種有效的行是:

utt_id_01001   gunzip -c /usr/data/file_010001.wav.gz |

每行的格式是:

<key>   <rxfilename>

對於Matrix型別的物件,我們還可以指定範圍,比如:

utt_id_01002 foo.ark:89142[0:51]
utt_id_01002 foo.ark:89142[0:51,89:100]
utt_id_01002 foo.ark:89142[,89:100]

Kaldi處理script檔案的每一行:先去掉每行首尾空格,然後根據中間的空格把每一行分成兩個部分,前面一部分就是Table的key,比如說,utt_id_01001,第二部分去掉範圍指定符後成為xfilename,例如 gunzip -c /usr/data/file_010001.wav.gz |。空行或者空xfilename是不允許的。

Note: 偏置是以位元組為單位的(byte offsets),比如,foo.ark:8432表示第8432位元組。位元組偏置會指向物件的開頭。對於binary資料,它指向的是"\0B"

Kaldi archive檔案格式:

Kaldi的archive格式比較簡單,如下:

token1   [something]token2   [something]token3   [something] …

也就是: (a token; then a space character; then the result of calling the Write function of the Holder) .

指定Table的格式:wspecifiers 和 rspecifiers

Table類要求有一個string來傳給建構函式或者Open函式。如果傳給的是TableWriter,這個string被稱為wspecifier,如果傳給的是RandomAcessTableReader或者SequentialTableReader,這個string就叫做rspecifier

std::string rspecifier1 = "scp:data/train.scp"; // script file.
std::string rspecifier2 = "ark:-"; // archive read from stdin.
// write to a gzipped text archive.
std::string wspecifier1 = "ark,t:| gzip -c > /some/dir/foo.ark.gz";
std::string wspecifier2 = "ark,scp:data/my.ark,data/my.ark";

通常,一個rspecifier或者一個wspecifier由逗號分隔的列表,這個列表包含arkscp其中的一個,以及一些有一個字母或者兩個字母組成的選項,然後是一個冒號,後面接rxfilename或者wxfilename。冒號之前可選項的順序無關緊要。

同時寫一個archive和一個script檔案

wspecifier的一種特殊情況:在冒號之前是"ark,scp",冒號之後是一個寫archivewxfilename,接一個逗號,然後接一個寫scriptwxfilename

“ark,scp:/some/dir/foo.ark,/some/dir/foo.scp”

這會同時寫一個archive和一個script檔案,後者的每一行形如 "utt_id /somedir/foo.ark:1234",其中的數字指定了便於隨機訪問的偏置。注意,指定的archive的wxfilename應該是普通的檔名,要不然得到的script檔案將不被Kaldi直接可讀。

wspecifier的有效可選項

允許的wspecifier可選項有如下一些:

  • “b” (binary) means write in binary mode (currently unnecessary as it’s always the default).
  • “t” (text) means write in text mode.
  • “f” (flush) means flush the stream after each write operation.
  • “nf” (no-flush) means don’t flush the stream after each write operation (would currently be pointless, but calling code can change the default).
  • “p” means permissive mode, which affects “scp:” wspecifiers where the scp file is missing some entries: the “p” option will cause it to silently not write anything for these files, and report no error.

用多個可選項的wspecifier例子:

“ark,t,f:data/my.ark”
“ark,scp,t,f:data/my.ark,|gzip -c > data/my.scp.gz”

rspecifier的有效可選項

當了解這些可選項的時候,要記住,當archive是一個pipe時(通常情況下都是),讀archive的程式碼是不能在archive中進行搜尋的。如果一個RandomAccessTableReader在讀一個archive檔案,程式碼可能需要在記憶體中存很多物件來避免之後重新請求,或者它可能需要搜尋檔案知道檔案末尾來找一個實際上檔案中沒有的key。以下列出的一些可選項可以避免這種情況。

  • “o” (once) is the user’s way of asserting to the RandomAccessTableReader code that each key will be queried only once. This stops it from having to keep already-read objects in memory just in case they are needed again.
  • “p” (permissive) instructs the code to ignore errors and just provide what data it can; invalid data is treated as not existing. In scp files, this means that a query to HasKey() forces the load of the corresponding file, so the code can know to return false if the file is corrupt. In archives, this option stops exceptions from being raised if the archive is corrupted or truncated (it will just stop reading at that point).
  • “s” (sorted) instructs the code that the keys in an archive being read are in sorted string order. For RandomAccessTableReader, this means that when HasKey() is called for some key not in the archive, it can return false as soon as it encounters a “higher” key; it won’t have to read till the end.
  • “cs” (called-sorted) instructs the code that the calls to HasKey() and Value() will be in sorted string order. Thus, if one of these functions is called for some string, the reading code can discard the objects for lower-numbered keys. This saves memory. In effect, “cs” represents the user’s assertion that some other archive that the program may be iterating over, is itself sorted.

例子如下:

“ark:o,s,cs:-”
“scp,p:data/my.scp”