Kaldi Notes (1), I/O in C++ Level


Kaldi I/O C++ Level 筆記, 主要介紹以下幾點, 以及它們在 Kaldi c++ 裡如何關聯:

  1. 標準 low-level I/O for Kaldi Object
  2. XXXHolder類別: 一個符合標準 low-level I/O 的類別
  3. Kaldi Table Object: <key,value> pairs 組成的 Kaldi 格式檔案 (scp, ark), 其中 value 為 XXXHolder 類別

標準 low-level I/O for Kaldi Object

Kaldi Object 有自己的標準 I/O 介面:

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

因此定義了該 Kaldi Class 如何針對 istream 讀取 (ostream 寫入). 在 Kaldi 中, istream/ostream 一般是由 Input/Output(在 util/kaldi-io.h 裡定義) 這個 class 來開啟的. 那為何不用一般的 c++ iostream 開啟一個檔案呢? 這是因為 Kaldi 想要支援更多樣的檔案開啟方式, 稱為 “Extended filenames: rxfilenames and wxfilenames“.

例如可以從 stdin/stdout, pipe, file 和 file with offset 讀取寫入, 詳細請看文檔的 “Extended filenames: rxfilenames and wxfilenames” 部分.

所以 Input/Ouput Class 會自動解析 rxfilenames/wxfilenames 然後開啟 istream/ostream. 開啟後, Kaldi Object 就可以透過標準的 I/O 介面呼叫 Read/Write 方法了. 官網範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
{ // 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);
}

有時候會看到更精簡的寫法如下

1
2
3
4
5
6
7
8
int main(int argc, char *argv[]) {
...
std::string rxfilenames = po.GetArg(1);
std::string wxfilenames = po.GetArg(2);
SomeKaldiClass my_object;
ReadKaldiObject(rxfilenames, &my_object);
WriteKaldiObject(my_object, wxfilenames, binary);
}

其中 ReadKaldiObject and WriteKaldiObject (defined in util/kaldi-io.h) 的作用只是將 Input/Output 開啟 xfilenames 為 iostream, 並傳給 my_object 的標準 I/O 介面包裝起 來而已. 擷取 define 片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <class C> void ReadKaldiObject(const std::string &filename, C *c) {
bool binary_in;
Input ki(filename, &binary_in);
c->Read(ki.Stream(), binary_in);
}
// Specialize the template for reading matrices, because we want to be able to
// support reading 'ranges' (row and column ranges), like foo.mat[10:20].
// 上面的 class C 如果是 Matrix<float> or Matrix<double> 的話, 使用下面兩個定義
// Note: 這種方式是 template 的 specialization, 同樣名稱的 template function or class 可以重複出現,只針對某些 type 客製化
template <> void ReadKaldiObject(const std::string &filename,
Matrix<float> *m);
template <> void ReadKaldiObject(const std::string &filename,
Matrix<double> *m);
template <class C> inline void WriteKaldiObject(const C &c,
const std::string &filename,
bool binary) {
Output ko(filename, binary);
c.Write(ko.Stream(), binary);
}

Kaldi Table Object

Table Object 不直接透過標準的 Read/Write 操作, 是因為 Table object 的構成是由 <key,value> pairs 組成的, 而 value 才會是一個符合標準 Read/Write 操作的 object. 這種 table 所需要的讀寫可能有很多方式, 譬如 sequential access, random access 等等, 因此單純的 Read/Write 比較不能滿足需求, 更需要的是要有 Next, Done, Key, Value 等等的操作方式. 例如以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}

主要有幾種 table classes:
TableWriter, SequentialTableReader, RandomAccessTableReader 等等, 都定義在 util/kaldi-table.h. 我們就以 SequentialTableReader 來舉例. 上面的範例 feature_reader 就是一個 SequentialTableReader, 他的 <key,value> pairs 中的 value 定義為 BaseFloatMatrixHolder 類別 (一個符合標準 low-level I/O 的 Kaldi Class, 等於是多一層包裝).

XXXHolder (如 KaldiObjectHolder, BasicHolder, BasicVectorHolder, BasicVectorVectorHolder, …) 指的是符合標準 low-level I/O 的 Kaldi Object, 因此這些 XXXHolder 都可以統一透過 Read/Write 來呼叫. 這些 Holder 的定義在 util/kaldi-holder.h.
另外 kaldi-holder.h 最後一行會 include kaldi-holder-inl.h. “-inl” 意思是 inline, 通常會放在相對應沒有 -inl 的 .h 最後面, 用來當作是 inline implementation 用.

SequentialTableReader 的定義在 “util/kaldi-table.h”, 擷取要介紹的片段:

1
2
3
4
5
6
7
8
9
10
11
template<class Holder>
class SequentialTableReader {
public:
typedef typename Holder::T T;
inline bool Done();
inline std::string Key();
T &Value();
void Next();
private:
SequentialTableReaderImplBase<Holder> *impl_;
}

Done(), Next(), Key(), and Value() 都可以從 feature_reader 看到如何使用, 應該很直覺, 而 Holder 的解釋上面說了. 剩下要說明的是這行 SequentialTableReaderImplBase<Holder> *impl_;. 在呼叫 SequentialTableReader 的 Next() 時, 他實際上呼叫的是 impl_ 的 Next(). 定義在 util/kaldi-table-inl.h 片段:

1
2
3
4
5
template<class Holder>
void SequentialTableReader<Holder>::Next() {
CheckImpl();
impl_->Next();
}

impl_ 的 class 宣告是 “SequentialTableReaderImplBase”, 該類別的角色是提供一個父類別, 實際上會根據 impl_ 真正的類別呼叫其對應的 Next(), 就是多型的使用. 現在假設 impl_ 真正的類別是 SequentialTableReaderArchiveImpl. 我們可以在 util/kaldi-table-inl.h 看到他的 Next (line 531) 實作如下:

1
2
3
4
5
6
7
8
9
virtual void Next() {
...
if (holder_.Read(is)) {
state_ = kHaveObject;
return;
} else {
...
}
}

到這才真正看到透過 XXXHolder 使用 low-level I/O 的 Read()!

Kaldi Codes 品質很高阿, 要花不少時間讀, 果然 c++ 底子還是太差了.


References

  1. Kaldi Project