parse_layered ¶
parse_layered(
edgelist: DataFrame,
*,
widths: Mapping[str, int] | None = None,
ranks: Mapping[str, int] | None = None,
) -> LayeredSpec
Parse a source/target edgelist into a LayeredSpec.
The parser sorts the nodes of a DAG into layers and turns the
edges entering each layer into one packed hop, ready for one
PackedLinear per hop. Reach for it when prior-knowledge
edges, such as genes into pathways, should run as a
layer-by-layer network; parse_adjacency is the
shared-state packed alternative, which also allows cycles.
Names sort alphabetically within a layer, and no hop mask is
allocated.
Two defaults shape the result, and one keyword changes each.
Every node is a single unit, one neuron, unless widths
makes it a block of several. Every node sits one layer above
its deepest parent, so its layer is the length of its longest
path from the inputs, unless ranks places it at a level
you choose.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
DataFrame
|
Edge table with required columns |
required |
|
mapping of str to int or numpy integer
|
Number of units per named node. A unit is one neuron: it
holds one number per sample. A node of width Unlike
The price is Omitted names, |
None
|
|
mapping of str to int or numpy integer
|
Level per named node, in place of longest-path ranking.
By default a node sits one layer above its deepest
parent, so two nodes that belong on the same level can
land in different layers only because one has a longer
chain of parents below it. Ranks change how nodes are grouped into layers, never what the network computes: every node keeps the same parents through the same weights. So they matter only where something treats a layer as a group:
|
None
|
Returns:
| Type | Description |
|---|---|
LayeredSpec
|
Frozen structure: layers, one |
Raises:
| Type | Description |
|---|---|
Kpnn2Error
|
If |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If |
See Also
parse_adjacency : Pack the same table into one state vector;
allows cycles and self-loops. Has no ranks argument.
PackedLinear.from_hop : Build the layer for one hop.
gather_hop_inputs : Build one hop's input from the saved layer
tensors.
Notes
Every named edge belongs to exactly one hop, the hop into its
target's layer, however many layers it spans. A hop whose
target layer has parents further back reads several layers:
its source columns are those whole layers concatenated in
ascending order, including nodes that feed nothing in this
hop. A skip edge is therefore an ordinary weight rather than
a dummy neuron or a second mechanism; skips only reports
it. Under longest-path ranking a hop always reads the
previous layer; with ranks it may omit that layer when
every parent is a skip.
Packed indices count units, not nodes: named edge A -> B
expands into a (k_B, k_A) block of live pairs, which is
one pair per named edge at the default width 1.
layer_dims counts units too, while layer_nodes and
Hop.source_nodes hold one name per node, so their
lengths differ once any node is wider than 1.
LayeredSpec.node_units and LayeredSpec.hop_units
find the units of one node.
Terminals below maximum depth (early outputs) are allowed,
and isolated nodes cannot appear, since the node set is the
union of source and target. to_dict() stores the
renumbered ranks only when they put some node in a different
layer than longest-path ranking does on the same edges.
Examples:
A chain A -> H -> C plus the skip A -> C. The hop into
C reads both earlier layers, so the skip is a packed pair
of that hop:
>>> import pandas as pd
>>> import kpnn2
>>> edgelist = pd.DataFrame(
... {"source": ["A", "H", "A"], "target": ["H", "C", "C"]}
... )
>>> spec = kpnn2.parse_layered(edgelist)
>>> spec.layer_nodes
(('A',), ('H',), ('C',))
>>> spec.hops[1].source_nodes
('A', 'H')
>>> spec.hops[1].source_index, spec.hops[1].target_index
((0, 1), (0, 0))
>>> spec.hops[1].to_mask().tolist()
[[1.0, 1.0]]
>>> spec.skips[0].source, spec.skips[0].target
('A', 'C')
With widths, a hidden node of width 2 owns two units. Each
layer keeps the same nodes, only its unit count grows, and the
named edge A -> H becomes a 2-by-1 block:
>>> spec = kpnn2.parse_layered(
... edgelist,
... widths={"H": 2},
... )
>>> spec.layer_nodes
(('A',), ('H',), ('C',))
>>> spec.layer_dims
(1, 2, 1)
>>> spec.layer_widths
((1,), (2,), (1,))
>>> spec.hops[0].to_mask().tolist()
[[1.0], [1.0]]
C now reads two numbers from H instead of one, next to
the one from A:
>>> spec.hops[1].to_mask().tolist()
[[1.0, 1.0, 1.0]]
With ranks, two nodes can share a layer even when their
chains of parents differ in length. By default Short sits
in layer 1, one above its only parent, while Long sits in
layer 2 behind Mid:
>>> siblings = pd.DataFrame(
... {
... "source": ["A", "A", "Mid"],
... "target": ["Short", "Mid", "Long"],
... }
... )
>>> kpnn2.parse_layered(siblings).layer_nodes
(('A',), ('Mid', 'Short'), ('Long',))
Giving both the same rank puts them in one layer, and
A -> Short becomes a skip edge. The edges and their
weights stay the same; only the grouping moves, so a head or
a normalization on the last layer now sees both nodes:
>>> spec = kpnn2.parse_layered(
... siblings,
... ranks={
... "A": 0,
... "Mid": 1,
... "Short": 2,
... "Long": 2,
... },
... )
>>> spec.layer_nodes
(('A',), ('Mid',), ('Long', 'Short'))
>>> spec.skips[0].source, spec.skips[0].target
('A', 'Short')