The neural network architects.

Our implementation of the networks will follow this architecture:

Feature extraction

When building complex networks it's better to build and test the smaller components first, then combine them together. This way we can also reuse the individual parts easily.

Convolutional block

This block takes the descriptor or fingerprint maps as input, and returns outputs of a max pooling layer.

  • Descriptor: 13*37*37 -> 48*37*37 -> 48*19*19
  • Fingerprint: 3*37*36 -> 48*37*36 -> 48*19*18

class Convnet[source]

Convnet(C_in=13, C_out=48, conv_size=13) :: Module

Convolutional feature extraction Block

Let's test it on the descriptor and fingerprint maps

convnet = Convnet()

i = torch.rand((10, 13, 37, 37))
o = convnet(i)
o.shape
/Users/olivier/opt/anaconda3/envs/molmap/lib/python3.6/site-packages/torch/nn/functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  ../c10/core/TensorImpl.h:1156.)
  return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)
torch.Size([10, 48, 19, 19])
convnet = Convnet(3, 48)

i = torch.rand((10, 3, 37, 36))
o = convnet(i)
o.shape
torch.Size([10, 48, 19, 18])

Inception block

After the convolutional block, the resulting feature maps will further pass through some inception blocks.

The inceptions implemented here are the naïve Google inceptions. It passes the input through multiple convolutional layers and then concatenate the output. This inception block is actually two smaller inception blocks bridged with a max pooling layer. First the small inception block:

  • Descriptor: 48*19*19 -> 3 outputs of 32*19*19 -> 96*19*19, |-> 96*10*10 -> 3 outputs of 64*10*10 -> 192*10*10
  • Fingerprint: 48*19*18 -> 3 outputs of 32*19*18 -> 96*19*18, |-> 96*10*9 -> 3 outputs of 64*10*9 -> 192*10*9

class Inception[source]

Inception(C_in=48, C_out=32, stride=1) :: Module

Naive Google Inception Block

inception = Inception()

i = torch.rand((10, 48, 19, 19))
o = inception(i)
o.shape
torch.Size([10, 96, 19, 19])
inception = Inception(96, 64)

i = torch.rand((10, 96, 10, 10))
o = inception(i)
o.shape
torch.Size([10, 192, 10, 10])
inception = Inception()

i = torch.rand((10, 48, 19, 18))
o = inception(i)
o.shape
torch.Size([10, 96, 19, 18])
inception = Inception(96, 64)

i = torch.rand((10, 96, 10, 9))
o = inception(i)
o.shape
torch.Size([10, 192, 10, 9])

And the double inception block:

class DoubleInception[source]

DoubleInception(C_in1=48, C_out1=32, stride1=1, C_in2=96, C_out2=64, stride2=1) :: Module

Double Inception Block

double_inception = DoubleInception()

i = torch.rand((10, 48, 19, 19))
o = double_inception(i)
o.shape
torch.Size([10, 192, 10, 10])
double_inception = DoubleInception()

i = torch.rand((10, 48, 19, 18))
o = double_inception(i)
o.shape
torch.Size([10, 192, 10, 9])

Global max pooling

There is no global max pooling layer in PyTorch but this is very easy to realise.

i = torch.rand((10, 192, 10, 10))
o = i.amax(dim=(-1, -2))
o.shape
torch.Size([10, 192])

Fully connected block

At the end of the network the data passes through several fully connected layers.

If the MolMap network is single path:

  • 192 -> 128 -> 32

And if double path:

  • 384 -> 256 -> 128 -> 32

class SinglePathFullyConnected[source]

SinglePathFullyConnected(C1=192, C2=128, C3=32) :: Module

Fully connected layers for single path MolMap nets

single_path_fully_connected = SinglePathFullyConnected()

i = torch.rand((10, 192))
o = single_path_fully_connected(i)
o.shape
torch.Size([10, 32])

class DoublePathFullyConnected[source]

DoublePathFullyConnected(C1=384, C2=256, C3=128, C4=32) :: Module

Fully connected layers for double paths MolMap nets

double_path_fully_connected = DoublePathFullyConnected()

i = torch.rand((10, 384))
o = double_path_fully_connected(i)
o.shape
torch.Size([10, 32])

Single Path Molecular Mapping network

Descriptor map or Fingerprint map only. The two feature maps use identical network structures and only differ in data shape. Note that we need to specify the number of channels for the feature maps when initialising the model, but the model should be able to handle feature maps with different dimensions.

  • descriptor: 13*37*37 -> 32
  • fingerprint: 3*37*36 -> 32

The output layer is not included.

class SinglePathMolMapNet[source]

SinglePathMolMapNet(conv_in=13, conv_size=13, FC=[128, 32]) :: Module

Single Path Molecular Mapping Network

single_path = SinglePathMolMapNet()

i = torch.rand((10, 13, 37, 37))
o = single_path(i)
o.shape
torch.Size([10, 32])
single_path = SinglePathMolMapNet(conv_in=3)

i = torch.rand((10, 3, 37, 36))
o = single_path(i)
o.shape
torch.Size([10, 32])

Double Path Molecular Mapping network

Both the descriptor map and Fingerprint map will pass through the convolutional block, then the double inception block, and their results are then combined, before finally pass through the fully connected layers.

After convolutional and double inception block:

  • descriptor: 13*37*37 -> 192*10*10
  • fingerprint: 3*37*36 -> 192*10*9

After global max pooling:

  • descriptor: 192*10*10 -> 192
  • fingerprint: 192*10*9 -> 192

After Concatenation and fully connected blocks:

  • 192 + 192 -> 384 -> 32

The output layer is not included.

class DoublePathMolMapNet[source]

DoublePathMolMapNet(conv_in1=13, conv_in2=3, conv_size=13, FC=[256, 128, 32]) :: Module

Double Path Molecular Mapping Network

double_path = DoublePathMolMapNet()

i1 = torch.rand((10, 13, 37, 37))
i2 = torch.rand((10, 3, 37, 36))
o = double_path(i1, i2)
o.shape
torch.Size([10, 32])

Resnet block

Currently not used

class Resnet[source]

Resnet(C, conv_size) :: Module

Naive Google Inception Block

resnet = Resnet(48, 5)

i = torch.rand((10, 48, 19, 18))
o = resnet(i)
o.shape
torch.Size([10, 48, 19, 18])