Builder grafik C++ adalah alat yang canggih untuk:
- Membuat grafik kompleks
- Grafik parameter (misalnya, menetapkan delegasi pada
InferenceCalculator
, mengaktifkan/menonaktifkan bagian grafik) - Menduplikasi grafik (misalnya, bukan grafik khusus CPU dan GPU di pbtxt Anda dapat memiliki satu kode yang membuat grafik yang diperlukan, membagikan sebanyak mungkin)
- Mendukung input/output grafik opsional
- Menyesuaikan grafik per platform
Penggunaan Dasar
Mari kita lihat bagaimana builder grafik C++ dapat digunakan untuk grafik sederhana:
# Graph inputs.
input_stream: "input_tensors"
input_side_packet: "model"
# Graph outputs.
output_stream: "output_tensors"
node {
calculator: "InferenceCalculator"
input_stream: "TENSORS:input_tensors"
input_side_packet: "MODEL:model"
output_stream: "TENSORS:output_tensors"
options: {
[drishti.InferenceCalculatorOptions.ext] {
# Requesting GPU delegate.
delegate { gpu {} }
}
}
}
Fungsi untuk membangun CalculatorGraphConfig
di atas mungkin terlihat seperti:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
// Requesting GPU delegate.
inference_opts.mutable_delegate()->mutable_gpu();
input_tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
Stream<std::vector<Tensor>> output_tensors =
inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
Ringkasan singkat:
- Gunakan
Graph::In/SideIn
untuk mendapatkan input grafik sebagaiStream/SidePacket
- Gunakan
Node::Out/SideOut
untuk mendapatkan output node sebagaiStream/SidePacket
- Gunakan
Stream/SidePacket::ConnectTo
untuk menghubungkan aliran dan paket samping untuk input node (Node::In/SideIn
) dan output grafik (Graph::Out/SideOut
)- Ada "pintasan" operator
>>
yang dapat Anda gunakan sebagai pengganti FungsiConnectTo
(Misalnya,x >> node.In("IN")
).
- Ada "pintasan" operator
Stream/SidePacket::Cast
digunakan untuk mentransmisikan streaming atau paket sampingAnyType
(Misalnya,Stream<AnyType> in = graph.In(0);
) ke jenis tertentu- Menggunakan jenis sebenarnya, bukan
AnyType
, akan menetapkan Anda ke jalur yang lebih baik untuk melepaskan kemampuan pembuat grafik dan meningkatkan grafik Anda keterbacaan.
- Menggunakan jenis sebenarnya, bukan
Penggunaan Lanjutan
Fungsi Utilitas
Mari kita ekstrak kode konstruksi inferensi ke dalam fungsi utilitas khusus untuk bantuan untuk keterbacaan dan penggunaan ulang kode:
// Updates graph to run inference.
Stream<std::vector<Tensor>> RunInference(
Stream<std::vector<Tensor>> tensors, SidePacket<TfLiteModelPtr> model,
const InferenceCalculatorOptions::Delegate& delegate, Graph& graph) {
auto& inference_node = graph.AddNode("InferenceCalculator");
auto& inference_opts =
inference_node.GetOptions<InferenceCalculatorOptions>();
*inference_opts.mutable_delegate() = delegate;
tensors.ConnectTo(inference_node.In("TENSORS"));
model.ConnectTo(inference_node.SideIn("MODEL"));
return inference_node.Out("TENSORS").Cast<std::vector<Tensor>>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<std::vector<Tensor>> input_tensors =
graph.In(0).SetName("input_tensors").Cast<std::vector<Tensor>>();
SidePacket<TfLiteModelPtr> model =
graph.SideIn(0).SetName("model").Cast<TfLiteModelPtr>();
InferenceCalculatorOptions::Delegate delegate;
delegate.mutable_gpu();
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Graph outputs.
output_tensors.SetName("output_tensors").ConnectTo(graph.Out(0));
return graph.GetConfig();
}
Hasilnya, RunInference
menyediakan antarmuka yang jelas yang menyatakan apa saja
input/output dan jenisnya.
Perangkat ini dapat digunakan kembali dengan mudah, misalnya hanya memerlukan beberapa baris jika Anda ingin menjalankan perintah inferensi model:
// Run first inference.
Stream<std::vector<Tensor>> output_tensors =
RunInference(input_tensors, model, delegate, graph);
// Run second inference on the output of the first one.
Stream<std::vector<Tensor>> extra_output_tensors =
RunInference(output_tensors, extra_model, delegate, graph);
Dan Anda tidak perlu menduplikasi nama dan tag (InferenceCalculator
,
TENSORS
, MODEL
) atau memperkenalkan konstanta khusus di sana-sini - semuanya
detailnya dilokalkan ke fungsi RunInference
.
Kelas Utilitas
Dan tentunya, ini bukan hanya tentang fungsi, dalam beberapa kasus, ini bermanfaat untuk memperkenalkan kelas utilitas yang dapat membantu membuat kode konstruksi grafik lebih mudah dibaca dan lebih sedikit kerentanan error.
MediaPipe menawarkan kalkulator PassThroughCalculator
, yang hanya meneruskan
melalui inputnya:
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
node {
calculator: "PassThroughCalculator"
input_stream: "float_value"
input_stream: "int_value"
input_stream: "bool_value"
# The order must be the same as for inputs (or you can use explicit indexes)
output_stream: "passed_float_value"
output_stream: "passed_int_value"
output_stream: "passed_bool_value"
}
Mari kita lihat kode konstruksi C++ yang mudah untuk membuat grafik di atas:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
auto& pass_node = graph.AddNode("PassThroughCalculator");
float_value.ConnectTo(pass_node.In("")[0]);
int_value.ConnectTo(pass_node.In("")[1]);
bool_value.ConnectTo(pass_node.In("")[2]);
Stream<float> passed_float_value = pass_node.Out("")[0].Cast<float>();
Stream<int> passed_int_value = pass_node.Out("")[1].Cast<int>();
Stream<bool> passed_bool_value = pass_node.Out("")[2].Cast<bool>();
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
Meskipun representasi pbtxt
mungkin rentan terhadap error (jika kita memiliki banyak input yang harus diteruskan
secara keseluruhan), kode C++ akan terlihat lebih buruk: tag kosong berulang dan panggilan Cast
. Mari kita
lihat bagaimana kami dapat lebih baik dengan memperkenalkan PassThroughNodeBuilder
:
class PassThroughNodeBuilder {
public:
explicit PassThroughNodeBuilder(Graph& graph)
: node_(graph.AddNode("PassThroughCalculator")) {}
template <typename T>
Stream<T> PassThrough(Stream<T> stream) {
stream.ConnectTo(node_.In(index_));
return node_.Out(index_++).Cast<T>();
}
private:
int index_ = 0;
GenericNode& node_;
};
Dan sekarang kode konstruksi grafik dapat terlihat seperti:
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Graph inputs.
Stream<float> float_value = graph.In(0).SetName("float_value").Cast<float>();
Stream<int> int_value = graph.In(1).SetName("int_value").Cast<int>();
Stream<bool> bool_value = graph.In(2).SetName("bool_value").Cast<bool>();
PassThroughNodeBuilder pass_node_builder(graph);
Stream<float> passed_float_value = pass_node_builder.PassThrough(float_value);
Stream<int> passed_int_value = pass_node_builder.PassThrough(int_value);
Stream<bool> passed_bool_value = pass_node_builder.PassThrough(bool_value);
// Graph outputs.
passed_float_value.SetName("passed_float_value").ConnectTo(graph.Out(0));
passed_int_value.SetName("passed_int_value").ConnectTo(graph.Out(1));
passed_bool_value.SetName("passed_bool_value").ConnectTo(graph.Out(2));
// Get `CalculatorGraphConfig` to pass it into `CalculatorGraph`
return graph.GetConfig();
}
Kini Anda tidak boleh memiliki urutan atau indeks yang salah dalam konstruksi kartu
kode dan simpan sebagian ketikan dengan menebak jenis untuk Cast
dari PassThrough
input teks.
Anjuran dan Larangan
Tentukan input grafik di bagian paling awal jika memungkinkan
Dalam kode di bawah ini:
- Mungkin sulit untuk menebak berapa banyak {i>input<i} yang Anda miliki dalam grafik.
- Dapat menjadi rawan kesalahan secara keseluruhan dan sulit dikelola di masa mendatang (misalnya, apakah indeks yang benar? nama? bagaimana jika beberapa input dihapus atau dijadikan opsional? dll.).
- Penggunaan ulang
RunSomething
dibatasi karena grafik lain mungkin memiliki perbedaan input
LON'T — contoh kode yang buruk.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Graph& graph) {
Stream<C> c = graph.In(2).SetName("c").Cast<C>(); // Bad.
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
// 10/100/N lines of code.
Stream<B> b = graph.In(1).SetName("b").Cast<B>() // Bad.
Stream<D> d = RunSomething(a, b, graph);
// ...
return graph.GetConfig();
}
Sebagai gantinya, tentukan input grafik Anda di bagian paling awal pembuat grafik:
TINDAKAN — contoh kode yang baik.
Stream<D> RunSomething(Stream<A> a, Stream<B> b, Stream<C> c, Graph& graph) {
// ...
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).SetName("a").Cast<A>();
Stream<B> b = graph.In(1).SetName("b").Cast<B>();
Stream<C> c = graph.In(2).SetName("c").Cast<C>();
// 10/100/N lines of code.
Stream<D> d = RunSomething(a, b, c, graph);
// ...
return graph.GetConfig();
}
Gunakan std::optional
jika Anda memiliki aliran input atau paket samping yang tidak
selalu didefinisikan dan meletakkannya di awal:
TINDAKAN — contoh kode yang baik.
std::optional<Stream<A>> a;
if (needs_a) {
a = graph.In(0).SetName(a).Cast<A>();
}
Menentukan output grafik di bagian paling akhir
Dalam kode di bawah ini:
- Mungkin sulit untuk menebak berapa banyak {i>output<i} yang Anda miliki dalam grafik.
- Dapat menjadi rawan kesalahan secara keseluruhan dan sulit dikelola di masa mendatang (misalnya, apakah indeks yang benar? nama? bagaimana jika beberapa outpus dihapus atau dijadikan opsional? dll.).
- Penggunaan ulang
RunSomething
dibatasi karena grafik lain mungkin memiliki output yang berbeda
DON'T — contoh kode yang buruk.
void RunSomething(Stream<Input> input, Graph& graph) {
// ...
node.Out("OUTPUT_F")
.SetName("output_f").ConnectTo(graph.Out(2)); // Bad.
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
node.Out("OUTPUT_D")
.SetName("output_d").ConnectTo(graph.Out(0)); // Bad.
// 10/100/N lines of code.
node.Out("OUTPUT_E")
.SetName("output_e").ConnectTo(graph.Out(1)); // Bad.
// 10/100/N lines of code.
RunSomething(input, graph);
// ...
return graph.GetConfig();
}
Sebagai gantinya, tentukan output grafik Anda di bagian paling akhir pembuat grafik:
TINDAKAN — contoh kode yang baik.
Stream<F> RunSomething(Stream<Input> input, Graph& graph) {
// ...
return node.Out("OUTPUT_F").Cast<F>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// 10/100/N lines of code.
Stream<D> d = node.Out("OUTPUT_D").Cast<D>();
// 10/100/N lines of code.
Stream<E> e = node.Out("OUTPUT_E").Cast<E>();
// 10/100/N lines of code.
Stream<F> f = RunSomething(input, graph);
// ...
// Outputs.
d.SetName("output_d").ConnectTo(graph.Out(0));
e.SetName("output_e").ConnectTo(graph.Out(1));
f.SetName("output_f").ConnectTo(graph.Out(2));
return graph.GetConfig();
}
Membuat node tetap terpisah satu sama lain
Di MediaPipe, aliran paket dan paket samping sama pentingnya dengan node. Dan setiap persyaratan input node serta produk output dinyatakan dengan jelas dan secara mandiri dalam hal {i>stream<i} dan paket {i> side<i} yang digunakannya, serta dihasilkan oleh perusahaan tersebut.
DON'T — contoh kode yang buruk.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
auto& node2 = graph.AddNode("Calculator2");
node1.Out("OUTPUT").ConnectTo(node2.In("INPUT")); // Bad.
auto& node3 = graph.AddNode("Calculator3");
node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C")); // Bad.
auto& node4 = graph.AddNode("Calculator4");
node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B")); // Bad.
node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C")); // Bad.
node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D")); // Bad.
// Outputs.
node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0)); // Bad.
node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1)); // Bad.
node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2)); // Bad.
node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3)); // Bad.
return graph.GetConfig();
}
Dalam kode di atas:
- Node digabungkan satu sama lain, misalnya
node4
tahu letak inputnya yang berasal dari (node1
,node2
,node3
) dan akan mempersulit pemfaktoran ulang, pemeliharaan dan penggunaan ulang kode- Pola penggunaan tersebut merupakan downgrade dari representasi proto, dengan node dipisahkan secara default.
- Panggilan
node#.Out("OUTPUT")
diduplikasi dan keterbacaan meningkat seiring Anda bisa menggunakan nama yang lebih bersih dan juga memberikan jenis yang sebenarnya.
Jadi, untuk memperbaiki masalah di atas, Anda dapat menulis kode konstruksi grafik berikut:
TINDAKAN — contoh kode yang baik.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
// `node1` usage is limited to 3 lines below.
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
// `node2` usage is limited to 3 lines below.
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
// `node3` usage is limited to 4 lines below.
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
// `node4` usage is limited to 5 lines below.
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Sekarang, jika perlu, Anda dapat dengan mudah menghapus node1
dan menjadikan b
sebagai input grafik dan
update diperlukan untuk node2
, node3
, node4
(sama seperti dalam representasi proto
karena mereka terpisah satu sama lain.
Secara keseluruhan, kode di atas mereplikasi grafik proto dengan lebih dekat:
input_stream: "a"
node {
calculator: "Calculator1"
input_stream: "INPUT:a"
output_stream: "OUTPUT:b"
}
node {
calculator: "Calculator2"
input_stream: "INPUT:b"
output_stream: "OUTPUT:C"
}
node {
calculator: "Calculator3"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
output_stream: "OUTPUT:d"
}
node {
calculator: "Calculator4"
input_stream: "INPUT_B:b"
input_stream: "INPUT_C:c"
input_stream: "INPUT_D:d"
output_stream: "OUTPUT:e"
}
output_stream: "b"
output_stream: "c"
output_stream: "d"
output_stream: "e"
Selain itu, sekarang Anda dapat mengekstrak fungsi utilitas untuk digunakan kembali dalam grafik lain:
TINDAKAN — contoh kode yang baik.
Stream<B> RunCalculator1(Stream<A> a, Graph& graph) {
auto& node = graph.AddNode("Calculator1");
a.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<B>();
}
Stream<C> RunCalculator2(Stream<B> b, Graph& graph) {
auto& node = graph.AddNode("Calculator2");
b.ConnectTo(node.In("INPUT"));
return node.Out("OUTPUT").Cast<C>();
}
Stream<D> RunCalculator3(Stream<B> b, Stream<C> c, Graph& graph) {
auto& node = graph.AddNode("Calculator3");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
return node.Out("OUTPUT").Cast<D>();
}
Stream<E> RunCalculator4(Stream<B> b, Stream<C> c, Stream<D> d, Graph& graph) {
auto& node = graph.AddNode("Calculator4");
b.ConnectTo(node.In("INPUT_B"));
c.ConnectTo(node.In("INPUT_C"));
d.ConnectTo(node.In("INPUT_D"));
return node.Out("OUTPUT").Cast<E>();
}
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Pisahkan node agar lebih mudah dibaca
DON'T — contoh kode yang buruk.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Pada kode di atas, mungkin sulit untuk memahami ide di mana setiap {i>node<i} dimulai dan berakhir. Untuk meningkatkannya dan membantu pembaca kode, Anda cukup mengosongkannya baris sebelum dan sesudah setiap {i>node<i}:
TINDAKAN — contoh kode yang baik.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
auto& node1 = graph.AddNode("Calculator1");
a.ConnectTo(node1.In("INPUT"));
Stream<B> b = node1.Out("OUTPUT").Cast<B>();
auto& node2 = graph.AddNode("Calculator2");
b.ConnectTo(node2.In("INPUT"));
Stream<C> c = node2.Out("OUTPUT").Cast<C>();
auto& node3 = graph.AddNode("Calculator3");
b.ConnectTo(node3.In("INPUT_B"));
c.ConnectTo(node3.In("INPUT_C"));
Stream<D> d = node3.Out("OUTPUT").Cast<D>();
auto& node4 = graph.AddNode("Calculator4");
b.ConnectTo(node4.In("INPUT_B"));
c.ConnectTo(node4.In("INPUT_C"));
d.ConnectTo(node4.In("INPUT_D"));
Stream<E> e = node4.Out("OUTPUT").Cast<E>();
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}
Selain itu, representasi di atas cocok dengan proto CalculatorGraphConfig
representasi yang lebih baik.
Jika Anda mengekstrak node ke dalam fungsi utilitas, node tersebut akan tercakup di dalam fungsi sudah jelas di mana mereka mulai dan berakhir, jadi tidak apa-apa untuk memiliki:
TINDAKAN — contoh kode yang baik.
CalculatorGraphConfig BuildGraph() {
Graph graph;
// Inputs.
Stream<A> a = graph.In(0).Cast<A>();
Stream<B> b = RunCalculator1(a, graph);
Stream<C> c = RunCalculator2(b, graph);
Stream<D> d = RunCalculator3(b, c, graph);
Stream<E> e = RunCalculator4(b, c, d, graph);
// Outputs.
b.SetName("b").ConnectTo(graph.Out(0));
c.SetName("c").ConnectTo(graph.Out(1));
d.SetName("d").ConnectTo(graph.Out(2));
e.SetName("e").ConnectTo(graph.Out(3));
return graph.GetConfig();
}