【學點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
或者ostream
。binary
引數是一個標誌位,表明要讀寫的是binary
text
資料。
Kaldi中的物件是怎麼儲存在檔案中的
上面我們提到,Kaldi進行讀操作的程式碼需要知道用哪種模式(binary mode
和 text mode
)。其實我們也不需要準確地追溯一個檔案到底是binary的還是text的。Kaldi物件的檔案提供給我們怎麼取辨識。一個binary的Kaldi檔案以字串"\0B"
開頭,而text檔案不需要檔案頭(header
)。
拓展檔名:rxfilenames 和 wxfilenames
rxfilename
和wxfilename
這兩個詞表示的不是類型別。它們只是一些經常出現在變數名字中的修飾符,它們的含義如下:
- 一個
rxfilename
Input
類所解釋。 - 一個
wxfilename
是一個字串,作為寫操作的拓展名被KaldiOutput
類所解釋。
{ // 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中表可以通過三種方式訪問:TableWriter
、SequentialTableReader
和 RandomAccessReader
。
這些都是模板,但是不是基於表所含的物件,二是基於Holder
型別。Holder
告訴訪問表的程式碼怎麼取讀寫表所包含的物件,它不是一個實際的類或者基類,而是描述了一系列以Holder
結尾的類,比如說,TokenHolder
和KaldiObjectHolder
。
被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
由逗號分隔的列表,這個列表包含ark
和scp
其中的一個,以及一些有一個字母或者兩個字母組成的選項,然後是一個冒號,後面接rxfilename
或者wxfilename
。冒號之前可選項的順序無關緊要。
同時寫一個archive和一個script檔案
wspecifier
的一種特殊情況:在冒號之前是"ark,scp"
,冒號之後是一個寫archive
的wxfilename
,接一個逗號,然後接一個寫script
的wxfilename
。
“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 whenHasKey()
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()
andValue()
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”