C++ 그래프 빌더는 다음을 위한 강력한 도구입니다.
- 복잡한 그래프 빌드
- 그래프 매개변수화 (예:
InferenceCalculator
에 대리자 설정, 그래프 일부 사용/사용 중지) - 중복 그래프 (예: pbtxt의 CPU 및 GPU 전용 그래프 대신) 필요한 그래프를 구성하는 단일 코드를 사용하여 가능한 한 많이)
- 선택적 그래프 입력/출력 지원
- 플랫폼별 그래프 맞춤설정
기본 사용법
간단한 그래프에 C++ 그래프 빌더를 사용하는 방법을 살펴보겠습니다.
# 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 {} }
}
}
}
위의 CalculatorGraphConfig
를 빌드하는 함수는 다음과 같습니다.
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();
}
간단한 요약:
Graph::In/SideIn
를 사용하여 그래프 입력을Stream/SidePacket
로 가져오기Node::Out/SideOut
를 사용하여 노드 출력을Stream/SidePacket
로 가져오기Stream/SidePacket::ConnectTo
를 사용하여 스트림과 사이드 패킷을 노드 입력 (Node::In/SideIn
) 및 그래프 출력 (Graph::Out/SideOut
) <ph type="x-smartling-placeholder">- </ph>
- '단축키'를 사용하면 연산자
>>
대신 사용할 수 있습니다.ConnectTo
함수 (예:x >> node.In("IN")
)
- '단축키'를 사용하면 연산자
Stream/SidePacket::Cast
는AnyType
의 스트림 또는 사이드 패킷을 전송하는 데 사용됩니다. (예:Stream<AnyType> in = graph.In(0);
)을 특정 유형으로 변환합니다. <ph type="x-smartling-placeholder">- </ph>
AnyType
대신 실제 유형을 사용하면 그래프 빌더 기능 활용 및 그래프 개선 있습니다.
고급 사용
유틸리티 함수
추론 구성 코드를 전용 유틸리티 함수로 추출하여 가독성과 코드 재사용에 도움이 됩니다.
// 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();
}
따라서 RunInference
은 무엇이 필요한지 설명하는 명확한 인터페이스를 제공합니다.
입력/출력과 그 유형을 나타냅니다.
쉽게 재사용할 수 있습니다. 예를 들어 추가 명령어를 실행하려는 경우 모델 추론:
// 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);
또한 중복 이름과 태그 (InferenceCalculator
,
TENSORS
, MODEL
)를 사용하거나 여기에 전용 상수를 도입할 수 있습니다.
세부정보는 RunInference
함수로 현지화됩니다.
유틸리티 클래스
물론 함수만이 아닙니다. 어떤 경우에는 그래프 생성 코드를 작성하는 데 도움이 되는 유틸리티 클래스 소개 가독성이 더 높아지고 오류 발생 가능성이 줄어듭니다.
MediaPipe는 PassThroughCalculator
계산기를 제공하며 이 계산기는
다음과 같습니다.
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"
}
위의 그래프를 만드는 간단한 C++ 구성 코드를 살펴보겠습니다.
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();
}
pbtxt
표현은 오류가 발생하기 쉽습니다 (전달할 입력이 많은 경우).
C++ 코드는 반복되는 빈 태그와 Cast
호출로 더 심하게 보입니다. 자,
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_;
};
이제 그래프 구성 코드가 다음과 같을 수 있습니다.
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();
}
이제 구성을 통해 패스에 잘못된 순서나 색인이 있어서는 안 됩니다.
PassThrough
에서 Cast
의 유형을 추측하여 입력 시간을 절약합니다.
있습니다.
권장사항 및 금지사항
가능하면 맨 처음에 그래프 입력을 정의하세요.
아래 코드에서 각 항목의 의미는 다음과 같습니다.
- 그래프에 몇 개의 입력이 있는지 추측하기는 어려울 수 있습니다.
- 전반적으로 오류가 발생하기 쉬우며 향후 유지관리가 어려울 수 있습니다 (예: 올바른 색인인가요? 이름은 무엇인가요? 일부 입력이 삭제되거나 선택사항으로 지정되면 어떻게 될까요? 등).
- 다른 그래프가 서로 다를 수 있으므로
RunSomething
재사용이 제한적입니다. 입력
하지 말아야 할 일 - 잘못된 코드의 예
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();
}
대신 그래프 빌더의 맨 시작 부분에서 그래프 입력을 정의하세요.
권장 - 좋은 코드의 예
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();
}
호환되지 않는 입력 스트림 또는 측 패킷이 있는 경우 std::optional
사용
항상 정의하고 맨 앞에 배치합니다.
권장 - 좋은 코드의 예
std::optional<Stream<A>> a;
if (needs_a) {
a = graph.In(0).SetName(a).Cast<A>();
}
맨 끝에서 그래프 출력 정의
아래 코드에서 각 항목의 의미는 다음과 같습니다.
- 그래프에 몇 개의 출력이 있는지 추측하기는 어려울 수 있습니다.
- 전반적으로 오류가 발생하기 쉬우며 향후 유지관리가 어려울 수 있습니다 (예: 올바른 색인인가요? 이름은 무엇인가요? 일부 출력물이 삭제되거나 선택사항으로 지정되면 어떻게 되나요? 등).
- 다른 그래프의 출력이 다를 수 있으므로
RunSomething
재사용이 제한적입니다.
하지 말아야 할 일 - 잘못된 코드의 예
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();
}
대신 그래프 빌더의 맨 끝에서 그래프 출력을 정의하세요.
권장 - 좋은 코드의 예
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();
}
노드를 서로 분리된 상태로 유지
MediaPipe에서 패킷 스트림과 부가 패킷은 노드라는 두 가지 리소스가 있습니다 또한 모든 노드 입력 요구사항과 출력 제품이 명확하게 표현됨 사용하는 스트림과 부가 패킷 측면에서 독립적으로 생성합니다.
하지 말아야 할 일 - 잘못된 코드의 예
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();
}
위 코드에서 각 항목은 다음과 같습니다.
- 노드는 서로 결합됩니다. 예:
node4
는 입력 위치를 알고 있습니다. (node1
,node2
,node3
)에서 비롯되고 리팩터링이 복잡하므로 유지보수 및 코드 재사용 <ph type="x-smartling-placeholder">- </ph>
- 이러한 사용 패턴은 proto 표현에서 다운그레이드된 것으로 기본적으로 분리되어 있습니다.
node#.Out("OUTPUT")
호출이 중복되어 가독성이 저하됨 대신 더 명확한 이름을 사용하고 실제 유형도 제공할 수 있습니다.
따라서 위의 문제를 해결하려면 다음과 같은 그래프 구성 코드를 작성하면 됩니다.
권장 - 좋은 코드의 예
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();
}
이제 필요한 경우 쉽게 node1
를 삭제하고 b
를 그래프 입력으로 만들 수 있습니다.
node2
, node3
, node4
(proto 표현과 동일)에 업데이트가 필요합니다.
서로 분리되어 있기 때문입니다.
전반적으로 위의 코드는 proto 그래프를 더 가깝게 복제합니다.
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"
게다가, 이제 다른 그래프에서 나중에 다시 사용하기 위해 유틸리티 함수를 추출할 수 있습니다.
권장 - 좋은 코드의 예
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();
}
가독성 향상을 위해 노드 분리
하지 말아야 할 일 - 잘못된 코드의 예
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();
}
위의 코드에서는 각 노드가 어디에서 시작되고 어떤 노드가 노드에서 실행되는지 종료. 이를 개선하고 코드 리더를 돕기 위해 각 노드 앞뒤의 행:
권장 - 좋은 코드의 예
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();
}
또한 위의 표현은 CalculatorGraphConfig
proto와 일치합니다.
개선할 수 있습니다
노드를 유틸리티 함수로 추출하면 함수 내에서 범위가 지정됩니다. 시작과 끝이 분명하기 때문에 있습니다.
권장 - 좋은 코드의 예
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();
}