5.3.3 GraN-DAG: Gradient-Based Neural DAG Learning
Note: This notebook uses PyDeepCausalML’s NOTEARSNonlinearMLP, which implements the same masked-MLP + differentiable acyclicity framework as GraN-DAG (Lachapelle et al., NeurIPS 2020).
GraN-DAG (Lachapelle et al., NeurIPS 2020) is a gradient-based method for learning nonlinear causal DAG structure from observational data. Its core idea is to model every conditional distribution \(p(X_i \mid \mathbf{X}_{\mathrm{pa}(i)})\) with a separate masked MLP — one network per node — and then extract a continuous, differentiable adjacency matrix directly from the neural network weights. This avoids the linearity assumption of NOTEARS and the VAE framing of DAG-GNN while retaining the same smooth acyclicity constraint framework.
The pipeline, illustrated in the figure below, has five sequential stages:
Masked MLPs per node — each node \(X_i\) gets its own MLP with LeakyReLU hidden layers. Input masking controls which candidate parent variables can influence each node’s output. The mask pattern can be pre-filtered by Preliminary Neighbour Selection (PNS) before training begins, reducing the search space for large \(d\).
Adjacency extraction (compute_A_phi) — the continuous weighted adjacency \(A_{\phi,ij}\) is computed from the MLP weight matrices by summing contributions along all paths through the network: \(A_{\phi,ij} = \sum_{\text{paths}} \prod_k [W^{(k)}]^\top\). The result is a non-negative \(d \times d\) matrix whose entry \((i,j)\) reflects how strongly \(X_j\) influences the conditional distribution of \(X_i\).
Acyclicity constraint — the differentiable constraint \(h(A_\phi) = \mathrm{tr}(\exp(A_\phi \odot A_\phi)) - d = 0\) is evaluated on the extracted adjacency. \(h = 0\) exactly when the graph is a valid DAG; any cycle causes \(h > 0\).
Augmented Lagrangian optimization — the full objective \(L(\phi, \lambda, \mu) = \mathcal{L}(\phi) + (\mu/2)\,h^2 + \lambda\,h\) is minimized by iterating three steps: (i) gradient descent on \(\phi\) via RMSprop or SGD; (ii) dual-variable updates \(\lambda \leftarrow \lambda + \mu h\) and penalty scaling \(\mu \leftarrow \eta\,\mu\); and (iii) convergence check \(h \leq 10^{-8}\). The entire loop repeats until the acyclicity constraint is satisfied to tolerance.
Post-processing and output — two pruning steps convert the continuous solution to a binary causal matrix. Threshold search (_to_dag) iterates candidate thresholds on \(A_\phi\) and selects the largest value that still yields a valid DAG. Jacobian pruning (jac_thresh) removes edges whose empirical Jacobian magnitude is near zero, eliminating spurious edges that survived thresholding. The final outputs are a binary \(d \times d\) causal matrix \(M\) and its corresponding directed acyclic graph.
Node 0 is Treatment (binarized at median) and node 9 is Outcome.
Code
treatment_idx, outcome_idx =0, 9T = (data[:, treatment_idx] > np.median(data[:, treatment_idx])).astype(int)Y = data[:, outcome_idx]parents_of_Y = np.where(learned_bin[:, outcome_idx] !=0)[0]adjustment_set = [i for i in parents_of_Y if i != treatment_idx]print("Adjustment set for backdoor:", adjustment_set)if adjustment_set: X_cate = np.column_stack([T, data[:, adjustment_set]])else: X_cate = T.reshape(-1, 1)rng = np.random.default_rng(42)tr = rng.choice(len(Y), size=int(0.8*len(Y)), replace=False)reg = LinearRegression().fit(X_cate[tr], Y[tr])cate_estimate = reg.coef_[0]naive_corr = np.corrcoef(T, Y)[0, 1]print("Estimated ATE (via adjustment):", cate_estimate)print("Validation: naive correlation:", naive_corr)
Adjustment set for backdoor: [np.int64(1), np.int64(2), np.int64(3), np.int64(4), np.int64(5), np.int64(6), np.int64(7), np.int64(8)]
Estimated ATE (via adjustment): -0.6815887826954514
Validation: naive correlation: -0.408498630450785
Validation of Structure
Interpreting the metrics. With the default edge rule (any nonzero weight), the masked-MLP recovers high recall — it tends to find the true edges — but low precision, because it also keeps many weak spurious edges (an over-connected graph). In the original GraN-DAG this is exactly what the post-processing stages (threshold search and Jacobian pruning) are for. Here you can trade recall for precision by thresholding learned_adj at a higher value before computing metrics.
GraN-DAG achieves nonlinear causal structure learning via masked MLPs and a differentiable acyclicity constraint. This notebook uses PyDeepCausalML’s NOTEARSNonlinearMLP, which follows the same gradient-based neural DAG learning paradigm.