# Build this neural network layer (NoisyAnd) more elegantly?

I want to build the neural network in "Classifying and segmenting microscopy images with deep multiple instance learning". It is a fairly straightforward CNN, except that they define a custom layer which they describe as Noisy-AND.

I'm able to define this operation as a NetGraph but it's surprisingly convoluted to do so. I'm wondering if anyone can help me express the same computation more elegantly.

The input is a vector of features for each output class (they are scaled between zero and one, and represent probabilities.) The first step is to take the mean of the feature vector for each class; call this $p_{i}$. This is easy enough with an AggregationLayer.

There are two learnable parameters: $a$, a single scalar for the whole network, and $b$, a vector of parameters, one $b_i$ for each class. These can be expressed using ConstantArrayLayer, although I need to use ReplicateLayer to express multiplying a vector by the scalar $a$.

The noisy-AND function, for a given class $i$, is then

$\begin{equation*} \frac{\sigma(a(p_i - b_i)) - \sigma(-a b_i)}{\sigma(a(1 - b_i)) - \sigma(- a b_i)} \end{equation*}$

That looks pretty simple and innocent. But here is some Mathematica code to generate a NetGraph. As you can see, it's pretty involved, although fortunately I can now just use it as a subcomponent in the much easier-to-define CNN.

Any way you can think of to simplify it would be helpful.

ClearAll[noisyAnd];
noisyAnd[numClasses_, numFeatures_] :=
NetGraph[<|
"a" -> ConstantArrayLayer["Array" -> {1.0}],
"b" ->
ConstantArrayLayer["Array" -> ConstantArray[0.5, numClasses]],
"mean" -> AggregationLayer[Mean],
"negatedSigmoid" -> ElementwiseLayer[LogisticSigmoid[-#] &],
"flattenA" -> FlattenLayer[],
"upperSigmoid" -> LogisticSigmoid,
"oneMinus" -> ElementwiseLayer[1.0 - # &],
"lowerSigmoid" -> LogisticSigmoid,
{
"mean" -> "minus1numerator",
"b" -> "minus1numerator",
(*compute sigmoid(-abi)*)
"flattenA" -> "multiply2",
"b" -> "multiply2",
"multiply2" -> "negatedSigmoid",
(*compute numerator*)

"minus1numerator" -> "multiply1numerator",
"flattenA" -> "multiply1numerator",
"multiply1numerator" -> "upperSigmoid",
"upperSigmoid" -> "minus2numerator",
"negatedSigmoid" -> "minus2numerator",
(*compute denominator*)
"b" -> "oneMinus",
"flattenA" -> "multiply1denominator",
"oneMinus" -> "multiply1denominator",
"multiply1denominator" -> "lowerSigmoid",
"lowerSigmoid" -> "minus2denominator",
"negatedSigmoid" -> "minus2denominator",
"minus2numerator" -> "quotient",
"minus2denominator" -> "quotient"
}, "Input" -> {numClasses, numFeatures}]


The main simplification is that the noisy-AND function is a single ThreadingLayer:
l = ThreadingLayer[(LogisticSigmoid[#1(#2 - #3)] -