Tensor Train layers for neural networks

In this section, the TT layers are introduced.

Imports:

[8]:
import torch as tn
import torch.nn as nn
import datetime
try:
    import torchtt as tntt
except:
    print('Installing torchTT...')
    %pip install git+https://github.com/ion-g-ion/torchTT
    import torchtt as tntt

We consider a linear layer \(\mathcal{LTT}(\mathsf{x}) = \mathsf{Wx}+\mathsf{b}\) acting on a tensor input \(\mathsf{x}\) of shape \(n_1 \times \cdots \times n_d\) and returning a tensor of shape \(m_1\times\cdots\times m_d\). The corresponding weight matrix \(\mathsf{W}\) would have the shape \((m_1\times\cdots\times m_d) \times (n_1 \times \cdots \times n_d)\). The goal is to represent the weights tensor operator in TT format and perform the learning with respect tot the cores of the TT decomposition (ranks have to be fixed a priori). Due to the AD functionality of torchtt, the gradient with respect tot the cores can be computed for any network structure. TT layers can be added using torchtt.nn.LinearLayerTT() class.

In the following, a neural netywork with 3 hidden layers and one linear layer is created. The shapes of the individual layers are

\(\mathbb{R}^{16} \times\mathbb{R}^{16} \times\mathbb{R}^{16} \times\mathbb{R}^{16} \underset{}{\longrightarrow} \mathbb{R}^8 \times\mathbb{R}^8 \times\mathbb{R}^8 \times\mathbb{R}^8 \underset{}{\longrightarrow} \mathbb{R}^4 \times\mathbb{R}^4 \times\mathbb{R}^4 \times\mathbb{R}^4 \underset{}{\longrightarrow} \mathbb{R}^2 \times\mathbb{R}^4 \times\mathbb{R}^2 \times\mathbb{R}^4 \underset{}{\longrightarrow} \mathbb{R}^{10}\).

[9]:
class BasicTT(nn.Module):
    def __init__(self):
        super().__init__()
        self.ttl1 = tntt.nn.LinearLayerTT([16,16,16,16], [8,8,8,8], [1,3,3,3,1])
        self.ttl2 = tntt.nn.LinearLayerTT([8,8,8,8], [4,4,4,4], [1,2,2,2,1])
        self.ttl3 = tntt.nn.LinearLayerTT([4,4,4,4], [2,4,2,4], [1,2,2,2,1])
        self.linear = nn.Linear(64, 10, dtype = tn.float32)

    def forward(self, x):
        x = self.ttl1(x)
        x = tn.relu(x)
        x = self.ttl2(x)
        x = tn.relu(x)
        x = self.ttl3(x)
        x = tn.relu(x)
        x = tn.reshape(x,[-1,64])
        return self.linear(x)

Create the model and print the number of trainable parameters as well as the model structure.

[10]:
model = BasicTT()
print('Number of trainable parameters:', len(list(model.parameters())))
print(model)

Number of trainable parameters: 17
BasicTT(
  (ttl1): LinearLayerTT(
    (cores): ParameterList(
        (0): Parameter containing: [torch.float32 of size 1x8x16x3]
        (1): Parameter containing: [torch.float32 of size 3x8x16x3]
        (2): Parameter containing: [torch.float32 of size 3x8x16x3]
        (3): Parameter containing: [torch.float32 of size 3x8x16x1]
    )
  )
  (ttl2): LinearLayerTT(
    (cores): ParameterList(
        (0): Parameter containing: [torch.float32 of size 1x4x8x2]
        (1): Parameter containing: [torch.float32 of size 2x4x8x2]
        (2): Parameter containing: [torch.float32 of size 2x4x8x2]
        (3): Parameter containing: [torch.float32 of size 2x4x8x1]
    )
  )
  (ttl3): LinearLayerTT(
    (cores): ParameterList(
        (0): Parameter containing: [torch.float32 of size 1x2x4x2]
        (1): Parameter containing: [torch.float32 of size 2x4x4x2]
        (2): Parameter containing: [torch.float32 of size 2x2x4x2]
        (3): Parameter containing: [torch.float32 of size 2x4x4x1]
    )
  )
  (linear): Linear(in_features=64, out_features=10, bias=True)
)

A random input is created and passed as argument to the model. Batch evaluation is also possible by extending the dimensionality of the input before the leading mode.

[11]:
input = tn.rand((16,16,16,16), dtype = tn.float32)
pred = model.forward(input)

input_batch = tn.rand((1000,16,16,16,16), dtype = tn.float32)
label_batch = tn.rand((1000,10), dtype = tn.float32)
%time pred = model.forward(input_batch)

CPU times: user 3.61 s, sys: 2.22 s, total: 5.84 s
Wall time: 450 ms

The obtained network can be trained similarily to other torch models. A loss function together with an optimizer are defined.

[12]:
criterion = nn.CrossEntropyLoss()
optimizer = tn.optim.Adam(model.parameters(), lr = 0.001)

A training loop is executed to exemplify the training parameters update procedure. An example where a true dataset is used is presented here.

[13]:
for epoch in range(5):

    optimizer.zero_grad()

    outputs = model(input_batch)
    loss = criterion(outputs, label_batch)
    loss.backward()
    optimizer.step()

    # print statistics
    print('Epoch %d, loss %e'%(epoch+1,loss.item()))


print('Finished Training')
%time plm = model(input_batch)
Epoch 1, loss 1.196611e+01
Epoch 2, loss 1.182479e+01
Epoch 3, loss 1.174133e+01
Epoch 4, loss 1.169423e+01
Epoch 5, loss 1.166731e+01
Finished Training
CPU times: user 3.85 s, sys: 1.79 s, total: 5.64 s
Wall time: 499 ms

If the GPU is available, the model can be run on it to get a speedup (should be run 2 times to see the speedup due to CUDA warm-up).

[14]:
if tn.cuda.is_available():
    model_gpu = BasicTT().cuda()
    input_batch_gpu = tn.rand((400,16,16,16,16)).cuda()

    input_batch = tn.rand((400,16,16,16,16))
    tme = datetime.datetime.now()
    pred = model.forward(input_batch)
    tme = datetime.datetime.now() - tme
    print('Time on CPU ',tme)

    tme = datetime.datetime.now()
    pred_gpu = model_gpu.forward(input_batch_gpu).cpu()
    tme = datetime.datetime.now() - tme
    print('Time on GPU ',tme)
Time on CPU  0:00:00.232971
Time on GPU  0:00:00.003855