การสร้างกราฟใน C++

เครื่องมือสร้างกราฟ C++ เป็นเครื่องมือที่มีประสิทธิภาพสำหรับ

  • การสร้างกราฟที่ซับซ้อน
  • กราฟที่ใช้พารามิเตอร์แบบพาราเมเตอร์ (เช่น การตั้งค่าผู้รับมอบสิทธิ์ใน InferenceCalculator การเปิด/ปิดใช้ส่วนต่างๆ ของกราฟ)
  • การกรองกราฟที่ซ้ำกันออก (เช่น แทนที่จะเป็นกราฟเฉพาะสำหรับ CPU และ GPU ใน pbtxt คุณสามารถมีโค้ดเดียวที่สร้างกราฟที่จำเป็น เพื่อแบ่งปัน ให้มากที่สุด)
  • รองรับอินพุต/เอาต์พุตกราฟที่ไม่บังคับ
  • การปรับแต่งกราฟต่อแพลตฟอร์ม

การใช้งานพื้นฐาน

มาดูกันว่าเครื่องมือสร้างกราฟ 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)
    • มี "ทางลัด" >> ที่คุณสามารถใช้แทน ConnectTo (เช่น x >> node.In("IN"))
  • Stream/SidePacket::Cast ใช้เพื่อแคสต์สตรีมหรือแพ็กเก็ตด้านข้างของ AnyType (เช่น Stream<AnyType> in = graph.In(0);) เป็นประเภทที่เฉพาะเจาะจง
    • การใช้ประเภทจริงแทน 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();
}

ตอนนี้คุณจะมีลำดับหรือดัชนีที่ไม่ถูกต้องในบัตรผ่านการก่อสร้างไม่ได้ และบันทึกการพิมพ์โดยเดาประเภท Cast จาก PassThrough อินพุต

สิ่งที่ควรและไม่ควรทำ

กำหนดอินพุตกราฟตั้งแต่เริ่มต้น หากเป็นไปได้

ในโค้ดด้านล่าง

  • การคาดเดาจำนวนอินพุตที่คุณมีในกราฟอาจเป็นเรื่องยาก
  • อาจเกิดความผิดพลาดในภาพรวมและยากที่จะดูแลรักษาในอนาคต (เช่น ดัชนีถูกต้องหรือไม่ ชื่อ จะเกิดอะไรขึ้นหากมีการนำข้อมูลบางส่วนออกหรือกำหนดให้ไม่บังคับ เป็นต้น)
  • มีการจำกัดการใช้ 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) ซึ่งทำให้เปลี่ยนโครงสร้างภายในโค้ดได้ง่าย การบำรุงรักษาและการนำโค้ดมาใช้ซ้ำ
    • รูปแบบการใช้งานดังกล่าวเป็นการดาวน์เกรดจากการแสดงโปรโต โดยโหนด จะถูกแยกออกโดยค่าเริ่มต้น
  • การโทร 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();
}