{"id":1823,"date":"2022-08-16T11:48:35","date_gmt":"2022-08-16T10:48:35","guid":{"rendered":"https:\/\/wp.coventry.domains\/e2edu\/?page_id=1823"},"modified":"2022-08-24T15:37:31","modified_gmt":"2022-08-24T14:37:31","slug":"pose-generation-with-a-gan","status":"publish","type":"page","link":"https:\/\/wp.coventry.domains\/e2edu\/pose-generation-with-a-gan\/","title":{"rendered":"Pose Generation with a GAN"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following tutorial introduces the use of a simple generative adversarial network for generating synthetic dance poses. This model can be trained on motion capture data. The model uses conventional neural network layers (ANN) to create poses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This tutorial forms part of a series of tutorials on using PyTorch to create and train generative deep learning models. The code for these tutorials is available&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/github.coventry.ac.uk\/ad5041\/PyTorch_ML_Tutorials\" target=\"_blank\">here<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After 400 epochs of training, the model generates pose that look like this when rendered as skeletons.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"341\" src=\"https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-1024x341.png\" alt=\"\" class=\"wp-image-1952\" srcset=\"https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-1024x341.png 1024w, https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-300x100.png 300w, https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-768x256.png 768w, https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-1536x512.png 1536w, https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs-788x263.png 788w, https:\/\/wp.coventry.domains\/e2edu\/wp-content\/uploads\/sites\/3486\/2022\/08\/PoseGan_Outputs.png 1728w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\">Imports<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The following modules need to be available and imported for this example. The \u201ccommon\u201d module with all its submodules is included when downloading the tutorial files.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import torch\nfrom torch.utils.data import Dataset\nfrom torch.utils.data import DataLoader\nfrom torch import nn\nfrom collections import OrderedDict\n\nimport os, sys, time, subprocess\nimport numpy as np\nsys.path.append(\"..\/..\")\n\nfrom common import utils\nfrom common.skeleton import Skeleton\nfrom common.mocap_dataset import MocapDataset\nfrom common.quaternion import qmul, qnormalize_np, slerp\nfrom common.pose_renderer import PoseRenderer\nimport matplotlib.pyplot as plt<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Compute Device<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If a GPU is available for running the model, this device can be selected as follows.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>device = 'cuda' if torch.cuda.is_available() else 'cpu'\nprint('Using {} device'.format(device))<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Read Motion Capture Data<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The file from which data is loaded is a \u201cpickled\u201d dictionary containing motion capture data. More information about this format is available\u00a0<a rel=\"noreferrer noopener\" href=\"https:\/\/wp.coventry.domains\/e2edu\/motion-capture-data\/\" target=\"_blank\">here<\/a>. The MocapDataset class is used to import such a file. Information about the MocapDataset class is available\u00a0<a rel=\"noreferrer noopener\" href=\"https:\/\/wp.coventry.domains\/e2edu\/utility-classes-and-functions\/\" target=\"_blank\">here<\/a>. In the following code excerpt, a motion capture file is loaded and a sequence of poses represented by joint rotations (quaternions) is obtained. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mocap_data_path = \"..\/..\/data\/Mocap\/MUR_Nov_2021\/MUR_PolytopiaMovement_Take2_mb_proc_rh.p\"\nmocap_fps = 50\n\n# load mocap data\nmocap_data = MocapDataset(mocap_data_path, fps=mocap_fps)\nif device == 'cuda':\n    mocap_data.cuda()\nmocap_data.compute_positions()\n\n# gather skeleton info\nskeleton = mocap_data.skeleton()\nskeleton_joint_count = skeleton.num_joints()\nskel_edge_list = utils.get_skeleton_edge_list(skeleton)\n\n# obtain pose sequence\nsubject = \"S1\"\naction = \"A1\"\npose_sequence = mocap_data&#091;subject]&#091;action]&#091;\"rotations\"]\n\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The edge list obtained from a skeleton can be passed to the constructor of the PoseRenderer class. An instance of the this class can be used for visualising poses. More information about the PoseRenderer class is available&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/wp.coventry.domains\/e2edu\/utility-classes-and-functions\/\" target=\"_blank\">here<\/a>. An instance of the PoseRenderer class can be created as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>skel_edge_list = utils.get_skeleton_edge_list(skeleton)\nposeRenderer = PoseRenderer(skel_edge_list)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Create Dataset<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To create a dataset from the motion capture data that can be used for training, several steps are undertaken: remove sequence excerpts in which poses are invalid or otherwise unsuitable for training, collect information about the remaining pose sequence, declare and define a Dataset class to hold the data, split the data into a training and test set, and instantiate DataLoaders from the training and test set.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The removal of unwanted sequence excerpts and the collection of information about the remaining pose sequence is conducted as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mocap_valid_frame_ranges = &#091; &#091; 500, 6500 ] ]\n\nposes = &#091;]\nfor valid_frame_range in mocap_valid_frame_ranges:\n    frame_range_start = valid_frame_range&#091;0]\n    frame_range_end = valid_frame_range&#091;1]\n    poses += &#091;pose_sequence&#091;frame_range_start:frame_range_end]]\nposes = np.concatenate(poses, axis=0)\n\npose_count = poses.shape&#091;0]\njoint_count = poses.shape&#091;1]\njoint_dim = poses.shape&#091;2]\npose_dim = joint_count * joint_dim\n\nposes = np.reshape(poses, (-1, pose_dim))<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A custom dataset class for poses is created by subclassing the Dataset class.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class PoseDataset(Dataset):\n    def __init__(self, poses):\n        self.poses = poses\n    \n    def __len__(self):\n        return self.poses.shape&#091;0]\n    \n    def __getitem__(self, idx):\n        return self.poses&#091;idx, ...]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This custom dataset class is instantiated as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>full_dataset = PoseDataset(poses)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This dataset contains all data. The dataset can be split into two datasets, one for training and one for testing, as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>test_percentage  = 0.2\n\ndataset_size = len(full_dataset)\n\ntest_size = int(test_percentage * dataset_size)\ntrain_size = dataset_size - test_size\n\ntrain_dataset, test_dataset = torch.utils.data.random_split(full_dataset, &#091;train_size, test_size])\n\ntrain_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\ntest_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">DataLoaders are created from these two datasets as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>batch_size = 16\n\ntrain_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\ntest_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Create Models<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">As has been explained in the <a rel=\"noreferrer noopener\" href=\"https:\/\/wp.coventry.domains\/e2edu\/image-generation-with-a-gan\/\" target=\"_blank\">previous article<\/a> on creating synthetic images with a GAN, both a Generator model and a Critique model need to be implemented. The Generator model takes as input a tensor containing noise and produces as output a tensor the represents synthetic poses. The Critique takes as input a tensor representing poses and produces as output a tensor that classifies the poses as ether real or fake. Both models employ only conventional artificial neural networks (ANN).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Create Critique Model<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For converting input poses into output classes, the Critique model passes the input poses in the form of a one dimensional feature vector through several ANN layers. These layers successively reduce the dimension of the feature vector down to 1. Each ANN layer with the exception of the last one is followed by a leaky ReLU activation function. The last ANN layer is followed by a Softmax activation function to obtain normalised class probabilities.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The class definition of the Critique model is as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class Critique(nn.Module):\n    def __init__(self, pose_dim, dense_layer_sizes):\n        super().__init__()\n        \n        self.pose_dim = pose_dim\n        self.dense_layer_sizes = dense_layer_sizes\n        \n        # create dense layers\n        dense_layers = &#091;]\n        \n        dense_layers.append((\"encoder_dense_0\", nn.Linear(self.pose_dim, self.dense_layer_sizes&#091;0])))\n        dense_layers.append((\"encoder_dense_relu_0\", nn.ReLU()))\n        \n        dense_layer_count = len(dense_layer_sizes)\n        for layer_index in range(1, dense_layer_count):\n            dense_layers.append( (\"encoder_dense_{}\".format(layer_index), nn.Linear(self.dense_layer_sizes&#091;layer_index-1], self.dense_layer_sizes&#091;layer_index] ) ) )\n            dense_layers.append( (\"encoder_dense_relu_{}\".format( layer_index ), nn.ReLU() ) )\n\n        dense_layers.append( ( \"encoder_dense_{}\".format( len( self.dense_layer_sizes ) ), nn.Linear( self.dense_layer_sizes&#091;-1], 1) ) )\n        dense_layers.append( ( \"encoder_dense_sigmoid_{}\".format( len( self.dense_layer_sizes ) ), nn.Sigmoid() ) )\n        \n        self.dense_layers = nn.Sequential(OrderedDict(dense_layers))\n        \n    def forward(self, x):\n        #print(\"x 1 \", x.shape\n        yhat = self.dense_layers(x)\n        #print(\"yhat \", yhat.shape)\n        return yhat<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The constructor of the Critique model class takes two arguments: the dimensions of a pose and a sequence of unit counts for the ANN layers (with the unit count of 1 for the last layer missing since this layer is added anyway). The model class can be instantiated as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>crit_dense_layer_sizes = &#091; 128, 64, 16 ]\n\ncritique = Critique(pose_dim, crit_dense_layer_sizes).to(device)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The shapes of the input and output tensors for this model are as follows:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>input tensor: batch_size x pose_dim<\/li><li>output tensor: batch_size x 1<\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Create Generator Model<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For generating synthetic poses from a one dimensional vector of random values, the Generator model passes the noise vector through ANN layers. These layers  successively increase the dimension of the noise vector. Each ANN layer with the exception of the last one is also followed by a ReLU activation function. The last ANN layer is not followed by an activation function.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The class definition of the Generator model is as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class Generator(nn.Module):\n    def __init__(self, pose_dim, latent_dim, dense_layer_sizes):\n        super(Generator, self).__init__()\n        \n        self.pose_dim = pose_dim\n        self.latent_dim = latent_dim\n        self.dense_layer_sizes = dense_layer_sizes\n\n        # create dense layers\n        dense_layers = &#091;]\n        \n        dense_layers.append((\"generator_dense_0\", nn.Linear(latent_dim, self.dense_layer_sizes&#091;0])))\n        dense_layers.append((\"generator_relu_0\", nn.ReLU()))\n\n        dense_layer_count = len(self.dense_layer_sizes)\n        for layer_index in range(1, dense_layer_count):\n            dense_layers.append((\"generator_dense_{}\".format(layer_index), nn.Linear(self.dense_layer_sizes&#091;layer_index-1], self.dense_layer_sizes&#091;layer_index])))\n            dense_layers.append( ( \"generator_dense_relu_{}\".format( layer_index ), nn.ReLU() ) )\n            \n        dense_layers.append( ( \"generator_dense_{}\".format( len( self.dense_layer_sizes ) ), nn.Linear( self.dense_layer_sizes&#091;-1], self.pose_dim) ) )\n \n        self.dense_layers = nn.Sequential(OrderedDict(dense_layers))\n        \n    def forward(self, x):\n        #print(\"x 1 \", x.size())\n        \n        # dense layers\n        yhat = self.dense_layers(x)\n        #print(\"yhat  \", yhat.size())\n\n\n        return yhat<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The constructor of the Generator model class takes three arguments: the pose dimension, the latent dimension of the pose encoding, and a sequence of unit counts for the ANN layers (with the unit count for the last layer missing  since this corresponds to the dimension  of a pose and this layer is added anyway). The model class can be instantiated as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>latent_dim = 8\n\ngenerator = Generator(pose_dim, latent_dim, gen_dense_layer_sizes).to(device)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The shapes of the input and output tensors for this model are as follows:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>input tensor: batch_size x latent_dim<\/li><li>output tensor: batch_size x pose_dim<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Optimisers and Loss Functions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The two optimisers and the loss function for the Critique model are identical to those used for the image generating GAN. What differs are the loss functions for the Generator. Two separate loss functions are used for the Generator. The loss function named &#8220;gen_crit_loss&#8221; is identical to the one named &#8220;gen_loss&#8221; in the image generating example. A second loss function named &#8220;gen_norm_loss&#8221; is used to quantify the deviation of the generated joint rotations from unit quaternions. The second loss function is defined as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def gen_norm_loss(yhat):\n    \n    _yhat = yhat.view(-1, 4)\n    _norm = torch.norm(_yhat, dim=1)\n    _diff = (_norm - 1.0) ** 2\n    _loss = torch.mean(_diff)\n    return _loss<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These two individual loss functions are called by the loss function named &#8220;gen_loss&#8221;. This loss function calculates a single loss value from a weighted sum of the two individual loss values. The function is defined as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>gen_norm_loss_scale = 0.1\ngen_crit_loss_scale = 1.0\n\ndef gen_loss(yhat, crit_fake_output):\n    _norm_loss = gen_norm_loss(yhat)\n    _crit_loss = gen_crit_loss(crit_fake_output)\n    \n    _total_loss = 0.0\n    _total_loss += _norm_loss * gen_norm_loss_scale\n    _total_loss += _crit_loss * gen_crit_loss_scale\n    \n    return _total_loss, _norm_loss, _crit_loss<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Training and Testing Functions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There is again a total of four different functions for conducting the training and testing steps for the Critique and Generator. These functions are extremely similar to the ones used for image generation. For this reason, the code for defining these functions is included here without further explanations.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def crit_train_step(real_poses, random_encodings):\n\n    critique_optimizer.zero_grad()\n\n    with torch.no_grad():\n        fake_output = generator(random_encodings)\n    real_output = real_poses\n        \n    crit_real_output =  critique(real_output)\n    crit_fake_output =  critique(fake_output)   \n    \n    _crit_loss = crit_loss(crit_real_output, crit_fake_output)\n        \n    _crit_loss.backward()\n    critique_optimizer.step()\n    \n    return _crit_loss\n\ndef crit_test_step(real_poses, random_encodings):\n    with torch.no_grad():\n        fake_output = generator(random_encodings)\n        real_output = real_poses\n        \n        crit_real_output =  critique(real_output)\n        crit_fake_output =  critique(fake_output)   \n    \n        _crit_loss = crit_loss(crit_real_output, crit_fake_output)\n\n    return _crit_loss\n\ndef gen_train_step(random_encodings):\n\n    generator_optimizer.zero_grad()\n\n    generated_poses = generator(random_encodings)\n    \n    crit_fake_output = critique(generated_poses)\n    \n    _gen_loss, _norm_loss, _crit_loss = gen_loss(generated_poses, crit_fake_output)\n    \n    _gen_loss.backward()\n    generator_optimizer.step()\n \n    return _gen_loss, _norm_loss, _crit_loss\n\ndef gen_test_step(random_encodings):\n    with torch.no_grad():\n        generated_poses = generator(random_encodings)\n    \n        crit_fake_output = critique(generated_poses)\n    \n        _gen_loss, _norm_loss, _crit_loss = gen_loss(generated_poses, crit_fake_output)\n    \n    return _gen_loss, _norm_loss, _crit_loss\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The function named \u201ctrain\u201d performs the actual training of the two models by calling the train and test step functions repeatedly. This function is also almost identical to the one used for image generation. The function is defined as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def train(train_dataloader, test_dataloader, epochs):\n    \n    loss_history = {}\n    loss_history&#091;\"gen train\"] = &#091;]\n    loss_history&#091;\"gen test\"] = &#091;]\n    loss_history&#091;\"crit train\"] = &#091;]\n    loss_history&#091;\"crit test\"] = &#091;]\n    loss_history&#091;\"gen crit\"] = &#091;]\n    loss_history&#091;\"gen norm\"] = &#091;]\n    \n    for epoch in range(epochs):\n\n        start = time.time()\n        \n        crit_train_loss_per_epoch = &#091;]\n        gen_train_loss_per_epoch = &#091;]\n        gen_norm_loss_per_epoch = &#091;]\n        gen_crit_loss_per_epoch = &#091;]\n        \n        for train_batch in train_dataloader:\n            train_batch = train_batch.to(device)\n            \n            random_encodings = torch.randn((train_batch.shape&#091;0], latent_dim)).to(device)\n\n            # start with critique training\n            _crit_train_loss = crit_train_step(train_batch, random_encodings)\n            \n            _crit_train_loss = _crit_train_loss.detach().cpu().numpy()\n\n            crit_train_loss_per_epoch.append(_crit_train_loss)\n            \n            # now train the generator\n            for iter in range(2):\n                _gen_loss, _gen_norm_loss, _gen_crit_loss = gen_train_step(random_encodings)\n            \n                _gen_loss = _gen_loss.detach().cpu().numpy()\n                _gen_norm_loss = _gen_norm_loss.detach().cpu().numpy()\n                _gen_crit_loss = _gen_crit_loss.detach().cpu().numpy()\n            \n                gen_train_loss_per_epoch.append(_gen_loss)\n                gen_norm_loss_per_epoch.append(_gen_norm_loss)\n                gen_crit_loss_per_epoch.append(_gen_crit_loss)\n\n        \n        crit_train_loss_per_epoch = np.mean(np.array(crit_train_loss_per_epoch))\n        gen_train_loss_per_epoch = np.mean(np.array(gen_train_loss_per_epoch))\n        gen_norm_loss_per_epoch = np.mean(np.array(gen_norm_loss_per_epoch))\n        gen_crit_loss_per_epoch = np.mean(np.array(gen_crit_loss_per_epoch))\n\n        crit_test_loss_per_epoch = &#091;]\n        gen_test_loss_per_epoch = &#091;]\n        \n        for test_batch in test_dataloader:\n            test_batch = test_batch.to(device)\n            \n            random_encodings = torch.randn((train_batch.shape&#091;0], latent_dim)).to(device)\n            \n            # start with critique testing\n            _crit_test_loss = crit_test_step(train_batch, random_encodings)\n            \n            _crit_test_loss = _crit_test_loss.detach().cpu().numpy()\n\n            crit_test_loss_per_epoch.append(_crit_test_loss)\n            \n            # now test the generator\n            _gen_loss, _, _ = gen_test_step(random_encodings)\n            \n            _gen_loss = _gen_loss.detach().cpu().numpy()\n            \n            gen_test_loss_per_epoch.append(_gen_loss)\n\n        crit_test_loss_per_epoch = np.mean(np.array(crit_test_loss_per_epoch))\n        gen_test_loss_per_epoch = np.mean(np.array(gen_test_loss_per_epoch))\n        \n        if epoch % weight_save_interval == 0 and save_weights == True:\n            torch.save(critique.state_dict(), \"results\/weights\/critique_weights_epoch_{}\".format(epoch))\n            torch.save(generator.state_dict(), \"results\/weights\/generator_weights_epoch_{}\".format(epoch))\n        \n        plot_gan_outputs(generator, epoch, n=5)\n\n        loss_history&#091;\"gen train\"].append(gen_train_loss_per_epoch)\n        loss_history&#091;\"gen test\"].append(gen_test_loss_per_epoch)\n        loss_history&#091;\"crit train\"].append(crit_train_loss_per_epoch)\n        loss_history&#091;\"crit test\"].append(crit_test_loss_per_epoch)\n        loss_history&#091;\"gen crit\"].append(gen_crit_loss_per_epoch)\n        loss_history&#091;\"gen norm\"].append(gen_norm_loss_per_epoch)\n\n        print ('epoch {} : gen train: {:01.4f} gen test: {:01.4f} crit train {:01.4f} crit test {:01.4f} gen norm {:01.4f} gen crit {:01.4f} time {:01.2f}'.format(epoch + 1, gen_train_loss_per_epoch, gen_test_loss_per_epoch, crit_train_loss_per_epoch, crit_test_loss_per_epoch, gen_norm_loss_per_epoch, gen_crit_loss_per_epoch, time.time()-start))\n    \n    return loss_history<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">As in the image generating example, the training function calls after each epoch a function to visually verify the progress of the training. This function draws an image in which several synthetic poses generated by the Generator are placed in a row.  The function employs an instance of the PoseRenderer class to render the synthetic poses. The function takes as arguments the Generator model, the current epoch, and the number of poses that the Generator should generate. This function is defined as follows.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def plot_gan_outputs(generator, epoch, n=5):\n    generator.eval()\n    \n    plt.figure(figsize=(10,4.5))\n    \n    zero_trajectory = torch.tensor(np.zeros((1, 1, 3), dtype=np.float32))\n    zero_trajectory = zero_trajectory.to(device)\n    \n    for i in range(n):\n        ax = plt.subplot(1,n,i+1)\n        \n        random_encoding = torch.randn((1, latent_dim)).to(device)\n        \n        with torch.no_grad():\n            gen_pose  = generator(random_encoding)\n            \n        gen_pose = torch.squeeze(gen_pose)\n        gen_pose = gen_pose.view((-1, 4))\n        gen_pose = nn.functional.normalize(gen_pose, p=2, dim=1)\n        gen_pose = gen_pose.view((1, 1, joint_count, joint_dim))\n        \n        skel_pose = skeleton.forward_kinematics(gen_pose, zero_trajectory)\n        skel_pose = skel_pose.detach().cpu().numpy()\n        skel_pose = np.reshape(skel_pose, (1, joint_count, 3))\n\n        view_min, view_max = utils.get_equal_mix_max_positions(skel_pose)\n        pose_image = poseRenderer.create_pose_images(skel_pose, view_min, view_max, view_ele, view_azi, view_line_width, view_size, view_size)\n        \n        plt.imshow(pose_image&#091;0])\n        ax.get_xaxis().set_visible(False)\n        ax.get_yaxis().set_visible(False)  \n        if i == 0:\n            ax.set_title(\"Epoch {}: Generated Images\".format(epoch))\n        \n    plt.show()\n        \n    generator.train()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The \u201ctrain\u201d function can be called as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>loss_history = train(train_dataloader, test_dataloader, epochs)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Generate and Visualise Poses <\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two convenience functions are defined for rendering poses as graphical images. The images are exported in \u201c<a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/GIF\" target=\"_blank\">.gif<\/a>\u201d format.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first function named \u201ccreate_ref_pose_image\u201d creates an image of a pose that is obtained from the original motion capture data. This function takes as arguments an index of the frame in a pose sequence and the name of the file the image is exported as.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def create_ref_pose_image(pose_index, file_name):\n    pose = poses&#091;pose_index]\n    pose = torch.tensor(np.reshape(pose, (1, 1, joint_count, joint_dim))).to(device)\n    zero_trajectory = torch.tensor(np.zeros((1, 1, 3), dtype=np.float32)).to(device)\n    skel_pose = skeleton.forward_kinematics(pose, zero_trajectory)\n    skel_pose = skel_pose.detach().cpu().numpy()\n    skel_pose = np.reshape(skel_pose, (joint_count, 3))\n    \n    view_min, view_max = utils.get_equal_mix_max_positions(skel_pose)\n    pose_image = poseRenderer.create_pose_image(skel_pose, view_min, view_max, view_ele, view_azi, view_line_width, view_size, view_size)\n    pose_image.save(file_name, optimize=False)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The second function named \u201ccreate_gen_pose_image\u201d creates an image of a synthetic pose that is generated by the Generator. This function takes as single argument the name of the file the image is exported as.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def create_gen_pose_image(file_name):\n    generator.eval()\n    \n    random_encoding = torch.randn((1, latent_dim)).to(device)\n    \n    with torch.no_grad():\n        gen_pose = generator(random_encoding)\n        \n    gen_pose = torch.squeeze(gen_pose)\n    gen_pose = gen_pose.view((-1, 4))\n    gen_pose = nn.functional.normalize(gen_pose, p=2, dim=1)\n    gen_pose = gen_pose.view((1, 1, joint_count, joint_dim))\n\n    zero_trajectory = torch.tensor(np.zeros((1, 1, 3), dtype=np.float32))\n    zero_trajectory = zero_trajectory.to(device)\n\n    skel_pose = skeleton.forward_kinematics(gen_pose, zero_trajectory)\n\n    skel_pose = skel_pose.detach().cpu().numpy()\n    skel_pose = np.squeeze(skel_pose)    \n\n    view_min, view_max = utils.get_equal_mix_max_positions(skel_pose)\n    pose_image = poseRenderer.create_pose_image(skel_pose, view_min, view_max, view_ele, view_azi, view_line_width, view_size, view_size)\n    pose_image.save(file_name, optimize=False)\n    \n    generator.train()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These two functions can be called as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pose_index = 100\n\ncreate_ref_pose_image(pose_index, \"results\/images\/orig_pose_{}.gif\".format(pose_index))\n\ncreate_gen_pose_image(\"results\/images\/gen_pose.gif\")<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Summary The following tutorial introduces the use of a simple generative adversarial network for generating synthetic dance poses. This model can be trained on motion capture data. The model uses conventional neural network layers (ANN) to create poses. This tutorial forms part of a series of tutorials on using PyTorch to create and train generative [&hellip;]<\/p>\n","protected":false},"author":2154,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","footnotes":""},"class_list":["post-1823","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/pages\/1823","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/users\/2154"}],"replies":[{"embeddable":true,"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/comments?post=1823"}],"version-history":[{"count":95,"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/pages\/1823\/revisions"}],"predecessor-version":[{"id":3301,"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/pages\/1823\/revisions\/3301"}],"wp:attachment":[{"href":"https:\/\/wp.coventry.domains\/e2edu\/wp-json\/wp\/v2\/media?parent=1823"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}