├── CHANGELOG.md ├── Guide.ipynb ├── LICENSE ├── Leaderboard.md ├── README.md ├── pyproject.toml ├── requirements.txt ├── src └── MLGeometry │ ├── __init__.py │ ├── bihomoNN.py │ ├── cicyhypersurface.py │ ├── complex_math.py │ ├── hypersurface.py │ ├── lbfgs.py │ ├── loss.py │ └── tf_dataset.py └── training ├── README.md ├── bihomoNN_train.py ├── bihomoNN_train.sh └── models.py /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [1.2.0] - 2025-03-07 6 | 7 | ### Changed 8 | 9 | - Updated the package to be compatible with the lastest version of Tensorflow (2.18) and Keras 3 10 | - The package can now be installed by pip 11 | - Moved the U1-invariant neural network from LOGML24 to the branch 'U1' 12 | 13 | ## [1.1.0] - 2023-11-20 14 | 15 | ### Added 16 | 17 | - A new section to print out the metrics explicitly in Guide.ipynb 18 | - Support for Calabi-Yau manifolds as the complete intersection of two hypersurfaces 19 | - Support for generating the real locus of a hypersurface with class RealHypersurface 20 | 21 | ### Changed 22 | 23 | - Changed the default initialization of the SquareDense layer to be all-positive with an extra 24 | abs function, which could help the training in certain cases 25 | - Changed several functions in the hypersurface class from being private to public 26 | 27 | ### Removed 28 | 29 | - An incorrect documentation for the complex hessian function 30 | - The function to do numerical integration over the manifold and several related deprecated functions 31 | 32 | ## [1.0.2] - 2022-03-18 33 | 34 | ### Added 35 | 36 | - A new argument d in the bihomogeneous layer for different dimensions 37 | - Save and load models in the guide 38 | - A tutorial for environment setup 39 | 40 | ### Removed 41 | 42 | - The n_patches attribute in the Hypersurface class since it fails on subpatches 43 | 44 | ## [1.0.1] - 2020-12-20 45 | 46 | ### Added 47 | 48 | - Multi-batch support for L-BFGS 49 | 50 | [Unreleased]: https://github.com/yidiq7/MLGeometry/compare/v1.2.0...HEAD 51 | [1.0.1]: https://github.com/yidiq7/MLGeometry/releases/tag/v1.0.1 52 | [1.0.2]: https://github.com/yidiq7/MLGeometry/releases/tag/v1.0.2 53 | [1.1.0]: https://github.com/yidiq7/MLGeometry/releases/tag/v1.1.0 54 | [1.2.0]: https://github.com/yidiq7/MLGeometry/releases/tag/v1.2.0 55 | -------------------------------------------------------------------------------- /Guide.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "2FIm6mUYtGC5" 7 | }, 8 | "source": [ 9 | "# MLGeometry guide" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": { 15 | "id": "dCemOECptGC6" 16 | }, 17 | "source": [ 18 | "This introduction demonstrates how to use MLGeometry to:\n", 19 | "1. Generate a hypersurface.\n", 20 | "2. Build a bihomogeneous neural network.\n", 21 | "3. Use the model to compute numerical Calabi-Yau metrics with the embedding method.\n", 22 | "4. Plot $\\eta$ on a rational curve." 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "source": [ 28 | "## Install the package (on Colab)" 29 | ], 30 | "metadata": { 31 | "id": "ilHaPYnkEi-S" 32 | } 33 | }, 34 | { 35 | "cell_type": "code", 36 | "source": [ 37 | "!pip install MLGeometry-tf" 38 | ], 39 | "metadata": { 40 | "id": "5pVEmL9vErvY" 41 | }, 42 | "execution_count": null, 43 | "outputs": [] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": { 48 | "id": "VyFvWKNmtGC7" 49 | }, 50 | "source": [ 51 | "## Configure imports" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": { 57 | "id": "lhqc2oMWtGC8" 58 | }, 59 | "source": [ 60 | "Import tensorflow_probability to use the L-BFGS optimizer:" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": { 67 | "id": "doBhWopntGC9" 68 | }, 69 | "outputs": [], 70 | "source": [ 71 | "import sympy as sp\n", 72 | "import tensorflow as tf\n", 73 | "import tensorflow_probability as tfp\n", 74 | "import keras" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": { 81 | "id": "MJJvGaCNtGC-" 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "import MLGeometry as mlg\n", 86 | "from MLGeometry import bihomoNN as bnn" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": { 92 | "id": "-9F3AKqPtGC_" 93 | }, 94 | "source": [ 95 | "Import the libraries to plot the $\\eta$ on the rational curve (see the last section):" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": null, 101 | "metadata": { 102 | "id": "J9LWSTH3tGC_" 103 | }, 104 | "outputs": [], 105 | "source": [ 106 | "import os\n", 107 | "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\n", 108 | "import math\n", 109 | "import numpy as np\n", 110 | "import matplotlib.pyplot as plt\n", 111 | "from mpl_toolkits.mplot3d import Axes3D" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": { 117 | "id": "pd1zG07TtGDA" 118 | }, 119 | "source": [ 120 | "## Set a random seed (optional)\n", 121 | "Some random seed might be bad for numerical calulations. If there are any errors during the training, you may want to try a different seed." 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": { 128 | "id": "x5Rz0lXmtGDB" 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "np.random.seed(42)\n", 133 | "tf.random.set_seed(42)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "markdown", 138 | "metadata": { 139 | "id": "QjB84Ln1tGDB" 140 | }, 141 | "source": [ 142 | "## Define a hypersurface\n", 143 | "First define a set of coordinates and a function as sympy symbols:" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": { 150 | "id": "pwhQynDBtGDB" 151 | }, 152 | "outputs": [], 153 | "source": [ 154 | "z0, z1, z2, z3, z4 = sp.symbols('z0, z1, z2, z3, z4')\n", 155 | "Z = [z0,z1,z2,z3,z4]\n", 156 | "f = z0**5 + z1**5 + z2**5 + z3**5 + z4**5 + 0.5*z0*z1*z2*z3*z4" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": { 162 | "id": "wTm215kftGDC" 163 | }, 164 | "source": [ 165 | "Then define a hypersurface as a collection of points which solve the equation f = 0, using the `Hypersurface` class in the `mlg.hypersurface` module. The parameter n_pairs is the number of random pairs of points used to form the random lines in $\\mathbf{CP}^{N+1}$. Then we take the intersections of those random lines and the hypersurface. By Bezout's theorem, each line intersects the hypersurface in precisely d points where d is the number of homogeneous coordinates. So the total number of points is d * n_pairs." 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "metadata": { 172 | "id": "jz1Vi4Y2tGDC" 173 | }, 174 | "outputs": [], 175 | "source": [ 176 | "n_pairs = 10000\n", 177 | "HS_train = mlg.hypersurface.Hypersurface(Z, f, n_pairs)\n", 178 | "HS_test = mlg.hypersurface.Hypersurface(Z, f, n_pairs)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": { 184 | "id": "YE981r2ctGDC" 185 | }, 186 | "source": [ 187 | "The Hypersurface class will take care of the patchwork automatically. Let's use the `list_patches` function to check the number of points on each patch:" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": { 194 | "id": "_zyw84dftGDC", 195 | "outputId": "e8fb2f30-eadf-4129-80c6-33971a79b273", 196 | "colab": { 197 | "base_uri": "https://localhost:8080/" 198 | } 199 | }, 200 | "outputs": [ 201 | { 202 | "output_type": "stream", 203 | "name": "stdout", 204 | "text": [ 205 | "Number of Patches: 5\n", 206 | "Points on patch 1 : 9933\n", 207 | "Points on patch 2 : 10015\n", 208 | "Points on patch 3 : 10249\n", 209 | "Points on patch 4 : 10011\n", 210 | "Points on patch 5 : 9792\n" 211 | ] 212 | } 213 | ], 214 | "source": [ 215 | "HS_train.list_patches()" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": { 221 | "id": "4JtfwpmFtGDD" 222 | }, 223 | "source": [ 224 | "You can also invoke this method on one of the patches to check the distribution on the subpatches:" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": { 231 | "id": "8fMZAyKutGDD", 232 | "outputId": "5b432e0a-36e6-4ac5-9dda-4c9447aa656a", 233 | "colab": { 234 | "base_uri": "https://localhost:8080/" 235 | } 236 | }, 237 | "outputs": [ 238 | { 239 | "output_type": "stream", 240 | "name": "stdout", 241 | "text": [ 242 | "Number of Patches: 4\n", 243 | "Points on patch 1 : 2474\n", 244 | "Points on patch 2 : 2468\n", 245 | "Points on patch 3 : 2547\n", 246 | "Points on patch 4 : 2444\n" 247 | ] 248 | } 249 | ], 250 | "source": [ 251 | "HS_train.patches[0].list_patches()" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": { 257 | "id": "h8p5D9ZWtGDE" 258 | }, 259 | "source": [ 260 | "The Hypersurface class contains some symbolic and numerical methods as well, which will be introduced elsewhere." 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": { 266 | "id": "4u1oIOLytGDE" 267 | }, 268 | "source": [ 269 | "## Training with Tensorflow\n", 270 | "The following steps are similar to a regular Tensorflow training process.\n", 271 | "### Generate datasets\n", 272 | "The `mlg.tf_dataset.generate_dataset` function converts a hypersurface to a Tensorflow Dataset, which has four componets: the points on the hypersurface, the volume form $\\small \\Omega \\wedge \\bar\\Omega$, the mass reweighting the points distribution and the restriction which restricts the Kähler metric to a subpatch. The restriction contains an extra linear transformation so that points on different affine patches can all be processed in one call. It is also possible to generate a dataset only on one affine patch." 273 | ] 274 | }, 275 | { 276 | "cell_type": "code", 277 | "execution_count": null, 278 | "metadata": { 279 | "id": "uGq-mKdDtGDE" 280 | }, 281 | "outputs": [], 282 | "source": [ 283 | "train_set = mlg.tf_dataset.generate_dataset(HS_train)\n", 284 | "test_set = mlg.tf_dataset.generate_dataset(HS_test)" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": { 290 | "id": "MHP3ExA1tGDG" 291 | }, 292 | "source": [ 293 | "Shuffle and batch the datasets:" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "metadata": { 300 | "id": "G1AX_cm_tGDG" 301 | }, 302 | "outputs": [], 303 | "source": [ 304 | "train_set = train_set.shuffle(HS_train.n_points).batch(1000)\n", 305 | "test_set = test_set.shuffle(HS_test.n_points).batch(1000)" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": { 311 | "id": "t7Mga5fdtGDG" 312 | }, 313 | "source": [ 314 | "Let's look at what is inside a dataset:" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": { 321 | "scrolled": true, 322 | "id": "kwHPcIFHtGDH", 323 | "outputId": "4e35f5b8-52d9-470a-caeb-9310229ee77a", 324 | "colab": { 325 | "base_uri": "https://localhost:8080/" 326 | } 327 | }, 328 | "outputs": [ 329 | { 330 | "output_type": "stream", 331 | "name": "stdout", 332 | "text": [ 333 | "tf.Tensor(\n", 334 | "[[-0.28939024-0.7013853j 0.36498785+0.31731284j 1. -0.j\n", 335 | " -0.83973217-0.04013346j 0.69238067-0.44357434j]\n", 336 | " [-0.2632903 +0.00763338j 1. +0.j -0.3904083 -0.03917403j\n", 337 | " -0.07155564+0.36113203j 0.80278397+0.58611786j]\n", 338 | " [-0.9368906 +0.12442758j 0.21154833+0.6303546j -0.3895686 +0.4952412j\n", 339 | " -0.22148535-0.86754274j 1. -0.j ]\n", 340 | " ...\n", 341 | " [-0.3757828 -0.8230363j -0.176058 +0.23105958j -0.36762843+0.82377625j\n", 342 | " -0.28792354-0.0678029j 1. -0.j ]\n", 343 | " [ 0.38600475+0.27455673j 0.80249757+0.5882626j 1. +0.j\n", 344 | " -0.0039644 +0.08065715j -0.46647677+0.11512545j]\n", 345 | " [ 0.8101118 -0.5754197j 1. +0.j -0.13839988-0.06433091j\n", 346 | " -0.17594224+0.5278059j -0.47444513+0.20035468j]], shape=(1000, 5), dtype=complex64)\n" 347 | ] 348 | } 349 | ], 350 | "source": [ 351 | "points, Omega_Omegabar, mass, restriction = next(iter(train_set))\n", 352 | "print(points)" 353 | ] 354 | }, 355 | { 356 | "cell_type": "markdown", 357 | "metadata": { 358 | "id": "2kVSGnnktGDH" 359 | }, 360 | "source": [ 361 | "### Build a bihomogeneous neural network" 362 | ] 363 | }, 364 | { 365 | "cell_type": "markdown", 366 | "metadata": { 367 | "id": "XV_JRrERtGDI" 368 | }, 369 | "source": [ 370 | "The `mlg.bihomoNN` module provides the necessary layers (e.g. `Bihomogeneous` and `Dense` ) to construct the Kähler potential with a bihomogeneous neural network. Here is an example of a two-hidden-layer network (k = 4) with 70 and 100 hidden units:" 371 | ] 372 | }, 373 | { 374 | "cell_type": "code", 375 | "execution_count": null, 376 | "metadata": { 377 | "id": "B9mE_YLltGDI" 378 | }, 379 | "outputs": [], 380 | "source": [ 381 | "@keras.saving.register_keras_serializable(package=\"MLGeometry\")\n", 382 | "class Kahler_potential(tf.keras.Model):\n", 383 | " def __init__(self, trainable=True, dtype='float32', **kwargs):\n", 384 | " super(Kahler_potential, self).__init__(trainable=trainable, dtype=dtype, **kwargs)\n", 385 | " # The first layer transforms the complex points to the bihomogeneous form.\n", 386 | " # The number of the outputs is d^2, where d is the number of coordinates.\n", 387 | " self.bihomogeneous = bnn.Bihomogeneous(d=len(Z))\n", 388 | " self.layer1 = bnn.SquareDense(5**2, 70, activation=tf.square)\n", 389 | " self.layer2 = bnn.SquareDense(70, 100, activation=tf.square)\n", 390 | " self.layer3 = bnn.SquareDense(100, 1)\n", 391 | "\n", 392 | " def call(self, inputs):\n", 393 | " x = self.bihomogeneous(inputs)\n", 394 | " x = self.layer1(x)\n", 395 | " x = self.layer2(x)\n", 396 | " x = self.layer3(x)\n", 397 | " x = tf.math.log(x)\n", 398 | " return x" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": null, 404 | "metadata": { 405 | "id": "cXSQuJgCtGDI" 406 | }, 407 | "outputs": [], 408 | "source": [ 409 | "model = Kahler_potential()" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": { 415 | "id": "vooMAXq2tGDJ" 416 | }, 417 | "source": [ 418 | "Define the Kähler metric $g_{i \\bar j} = \\partial_i\\bar\\partial_{\\bar j} K$ and the volume form $d\\mu_g = \\det g_{i \\bar j}$:" 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": null, 424 | "metadata": { 425 | "id": "ZKNPTfxPtGDJ" 426 | }, 427 | "outputs": [], 428 | "source": [ 429 | "@tf.function\n", 430 | "def volume_form(points, Omega_Omegabar, mass, restriction):\n", 431 | "\n", 432 | " kahler_metric = mlg.complex_math.complex_hessian(tf.math.real(model(points)), points)\n", 433 | " kahler_metric = tf.matmul(restriction, tf.matmul(kahler_metric, restriction, adjoint_b=True))\n", 434 | " det_g = tf.math.real(tf.linalg.det(kahler_metric))\n", 435 | "\n", 436 | " # Calculate the normalization constant to make the overall integration as 1\n", 437 | " # It is a batchwise calculation but we expect it to converge to a constant eventually\n", 438 | " # Consequently, if one computes the average of volume_form / Omega_Omegabar,\n", 439 | " # they will get strictly 1. (Actually the result would be Vol_Omega, but we set\n", 440 | " # it to be 1 here implicitly.)\n", 441 | " weights = mass / tf.reduce_sum(mass)\n", 442 | " factor = tf.reduce_sum(weights * det_g / Omega_Omegabar)\n", 443 | " volume_form = det_g / factor\n", 444 | "\n", 445 | " return volmue_form" 446 | ] 447 | }, 448 | { 449 | "cell_type": "markdown", 450 | "metadata": { 451 | "id": "GSvy2QCmtGDJ" 452 | }, 453 | "source": [ 454 | "### Train the model with Adam and L-BFGS\n", 455 | "#### Adam\n", 456 | "Setup the keras optmizer as `Adam` and the loss function as one of weighted loss in the `mlg.loss` module. Some available functions are `weighted_MAPE`, `weighted_MSE`, `max_error` and `MAPE_plus_max_error`. They are weighted with the mass formula since the points on the hypersurface are distributed according to the Fubini-Study measure while the measure used in the integration is determined by the volume form $\\small \\Omega \\wedge \\bar\\Omega$." 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": null, 462 | "metadata": { 463 | "id": "6jeIUXSTtGDK" 464 | }, 465 | "outputs": [], 466 | "source": [ 467 | "optimizer = keras.optimizers.Adam()\n", 468 | "loss_func = mlg.loss.weighted_MAPE" 469 | ] 470 | }, 471 | { 472 | "cell_type": "markdown", 473 | "metadata": { 474 | "id": "V8R-_rO7tGDK" 475 | }, 476 | "source": [ 477 | "Loop over the batches and train the network:" 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": null, 483 | "metadata": { 484 | "scrolled": true, 485 | "id": "WNWbQo1LtGDK", 486 | "outputId": "0a6b8726-c99d-41dc-9a56-6bafc1e9d172", 487 | "colab": { 488 | "base_uri": "https://localhost:8080/" 489 | } 490 | }, 491 | "outputs": [ 492 | { 493 | "output_type": "stream", 494 | "name": "stdout", 495 | "text": [ 496 | "epoch 10: loss = 0.04700\n", 497 | "epoch 20: loss = 0.01388\n", 498 | "epoch 30: loss = 0.01273\n", 499 | "epoch 40: loss = 0.01305\n", 500 | "epoch 50: loss = 0.01215\n", 501 | "epoch 60: loss = 0.01275\n", 502 | "epoch 70: loss = 0.01238\n", 503 | "epoch 80: loss = 0.01209\n", 504 | "epoch 90: loss = 0.01222\n", 505 | "epoch 100: loss = 0.01200\n", 506 | "epoch 110: loss = 0.01204\n", 507 | "epoch 120: loss = 0.01120\n", 508 | "epoch 130: loss = 0.01224\n", 509 | "epoch 140: loss = 0.01139\n", 510 | "epoch 150: loss = 0.01236\n", 511 | "epoch 160: loss = 0.01149\n", 512 | "epoch 170: loss = 0.01203\n", 513 | "epoch 180: loss = 0.01190\n", 514 | "epoch 190: loss = 0.01039\n", 515 | "epoch 200: loss = 0.01261\n" 516 | ] 517 | } 518 | ], 519 | "source": [ 520 | "max_epochs = 200\n", 521 | "epoch = 0\n", 522 | "while epoch < max_epochs:\n", 523 | " epoch = epoch + 1\n", 524 | " for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set):\n", 525 | " with tf.GradientTape() as tape:\n", 526 | " det_omega = volume_form(points, Omega_Omegabar, mass, restriction)\n", 527 | " loss = loss_func(Omega_Omegabar, det_omega, mass)\n", 528 | " grads = tape.gradient(loss, model.trainable_weights)\n", 529 | " optimizer.apply_gradients(zip(grads, model.trainable_weights))\n", 530 | " if epoch % 10 == 0:\n", 531 | " print(\"epoch %d: loss = %.5f\" % (epoch, loss))" 532 | ] 533 | }, 534 | { 535 | "cell_type": "markdown", 536 | "metadata": { 537 | "id": "uAVB6kgztGDL" 538 | }, 539 | "source": [ 540 | "Let's check the loss of the test dataset. First define a function to calculate the total loss over the whole dataset:" 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": null, 546 | "metadata": { 547 | "id": "SqGWnTq6tGDL" 548 | }, 549 | "outputs": [], 550 | "source": [ 551 | "def cal_total_loss(dataset, loss_function):\n", 552 | " total_loss = tf.constant(0, dtype=tf.float32)\n", 553 | " total_mass = tf.constant(0, dtype=tf.float32)\n", 554 | "\n", 555 | " for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset):\n", 556 | " det_omega = volume_form(points, Omega_Omegabar, mass, restriction)\n", 557 | " mass_sum = tf.reduce_sum(mass)\n", 558 | " total_loss += loss_function(Omega_Omegabar, det_omega, mass) * mass_sum\n", 559 | " total_mass += mass_sum\n", 560 | " total_loss = total_loss / total_mass\n", 561 | "\n", 562 | " return total_loss.numpy()" 563 | ] 564 | }, 565 | { 566 | "cell_type": "markdown", 567 | "metadata": { 568 | "id": "iDGmTMIytGDL" 569 | }, 570 | "source": [ 571 | "Check the results of MAPE and MSE:" 572 | ] 573 | }, 574 | { 575 | "cell_type": "code", 576 | "execution_count": null, 577 | "metadata": { 578 | "id": "N7gZJSOdtGDL", 579 | "outputId": "2bbba998-8388-4ac7-be8b-ede3299e4642", 580 | "colab": { 581 | "base_uri": "https://localhost:8080/" 582 | } 583 | }, 584 | "outputs": [ 585 | { 586 | "output_type": "stream", 587 | "name": "stdout", 588 | "text": [ 589 | "sigma_test = 0.01207\n", 590 | "E_test = 0.00027\n" 591 | ] 592 | } 593 | ], 594 | "source": [ 595 | "sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE)\n", 596 | "E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE)\n", 597 | "print(\"sigma_test = %.5f\" % sigma_test)\n", 598 | "print(\"E_test = %.5f\" % E_test)" 599 | ] 600 | }, 601 | { 602 | "cell_type": "markdown", 603 | "metadata": { 604 | "id": "omQZi7TytGDL" 605 | }, 606 | "source": [ 607 | "You can also check the error of the Monte Carlo integration, estimated by:\n", 608 | "\n", 609 | "$$\\delta \\sigma = \\frac{1}{\\sqrt{N_p}} {\\left( \\int_X (|\\eta - 1_X| - \\sigma)^2 d\\mu_{\\Omega}\\right)}^{1/2},$$\n", 610 | "\n", 611 | "where $N_p$ is the number of points on the hypersurface and $\\sigma$ is the `weighted_MAPE` loss, and\n", 612 | "\n", 613 | "$$\\eta = \\frac{\\det \\omega}{\\small \\Omega \\wedge \\bar \\Omega}$$" 614 | ] 615 | }, 616 | { 617 | "cell_type": "code", 618 | "execution_count": null, 619 | "metadata": { 620 | "id": "ZY2fn1iFtGDM", 621 | "outputId": "7c1f7b56-325c-434e-f46d-907a89e3857a", 622 | "colab": { 623 | "base_uri": "https://localhost:8080/" 624 | } 625 | }, 626 | "outputs": [ 627 | { 628 | "output_type": "stream", 629 | "name": "stdout", 630 | "text": [ 631 | "delta_simga = 0.00012\n" 632 | ] 633 | } 634 | ], 635 | "source": [ 636 | "def delta_sigma_square_test(y_true, y_pred, mass):\n", 637 | " weights = mass / tf.reduce_sum(mass)\n", 638 | " return tf.reduce_sum((tf.abs(y_true - y_pred) / y_true - sigma_test)**2 * weights)\n", 639 | "\n", 640 | "delta_sigma = cal_total_loss(test_set, delta_sigma_square_test)\n", 641 | "print(\"delta_simga = %.5f\" % delta_sigma)" 642 | ] 643 | }, 644 | { 645 | "cell_type": "markdown", 646 | "metadata": { 647 | "id": "HQxGKzH4tGDM" 648 | }, 649 | "source": [ 650 | "#### Save and Load\n", 651 | "The trained network can be saved by:" 652 | ] 653 | }, 654 | { 655 | "cell_type": "code", 656 | "execution_count": null, 657 | "metadata": { 658 | "id": "IkrLO0eutGDM" 659 | }, 660 | "outputs": [], 661 | "source": [ 662 | "os.makedirs('trained_model', exist_ok=True)\n", 663 | "model.save('trained_model/70_100_1.keras')" 664 | ] 665 | }, 666 | { 667 | "cell_type": "markdown", 668 | "metadata": { 669 | "id": "HHK4cU7VtGDN" 670 | }, 671 | "source": [ 672 | "And loaded by the `load_model` method:" 673 | ] 674 | }, 675 | { 676 | "cell_type": "code", 677 | "execution_count": null, 678 | "metadata": { 679 | "id": "CdjCeKlmtGDO" 680 | }, 681 | "outputs": [], 682 | "source": [ 683 | "model = keras.saving.load_model('trained_model/70_100_1.keras')" 684 | ] 685 | }, 686 | { 687 | "cell_type": "markdown", 688 | "metadata": { 689 | "id": "dlWwMrv7tGDP" 690 | }, 691 | "source": [ 692 | "#### L-BFGS\n", 693 | "As elaborated in our paper, when the network getting more complicated, L-BFGS converges faster than Adam near the minima. It is recommanded to use it after pretraining with Adam. However, L-BFGS is not in the standard Tensorflow library so the training process is slightly different: (Only ~20 iterations are shown here. In a real case you may want to set the `max_epochs` to ~1000)" 694 | ] 695 | }, 696 | { 697 | "cell_type": "code", 698 | "execution_count": null, 699 | "metadata": { 700 | "id": "3FUgvhWvtGDP", 701 | "outputId": "bf02ba35-d8c3-4ab1-d2d1-79e0c2345187", 702 | "colab": { 703 | "base_uri": "https://localhost:8080/" 704 | } 705 | }, 706 | "outputs": [ 707 | { 708 | "output_type": "stream", 709 | "name": "stdout", 710 | "text": [ 711 | "Iter: 1 loss: 0.0120117264\n", 712 | "Iter: 2 loss: 0.521354854\n", 713 | "Iter: 3 loss: 0.0819888934\n", 714 | "Iter: 4 loss: 0.0208096653\n", 715 | "Iter: 5 loss: 0.0115710068\n", 716 | "Iter: 6 loss: 0.0107686864\n", 717 | "Iter: 7 loss: 0.016376676\n", 718 | "Iter: 8 loss: 0.0107517727\n", 719 | "Iter: 9 loss: 0.0106189838\n", 720 | "Iter: 10 loss: 0.0111169312\n", 721 | "Iter: 11 loss: 0.0105820298\n", 722 | "Iter: 12 loss: 0.010477283\n", 723 | "Iter: 13 loss: 0.0104977814\n", 724 | "Iter: 14 loss: 0.0103970384\n", 725 | "Iter: 15 loss: 0.010341051\n", 726 | "Iter: 16 loss: 0.0110788839\n", 727 | "Iter: 17 loss: 0.0103399819\n" 728 | ] 729 | } 730 | ], 731 | "source": [ 732 | "# The displayed max_epochs will be three to four times this value since iter + 1 everytime the function\n", 733 | "# is invoked, which also happens during the evaluation of the function itself and its gradient\n", 734 | "max_epochs = 5\n", 735 | "\n", 736 | "# Setup the function to be optimized by L-BFGS\n", 737 | "\n", 738 | "train_func = mlg.lbfgs.function_factory(model, loss_func, train_set)\n", 739 | "\n", 740 | "# Setup the inital values and train\n", 741 | "init_params = tf.dynamic_stitch(train_func.idx, model.trainable_variables)\n", 742 | "results = tfp.optimizer.lbfgs_minimize(value_and_gradients_function=train_func,\n", 743 | " initial_position=init_params,\n", 744 | " max_iterations=max_epochs)\n", 745 | "# Update the model after the last loop\n", 746 | "train_func.assign_new_model_parameters(results.position)" 747 | ] 748 | }, 749 | { 750 | "cell_type": "markdown", 751 | "metadata": { 752 | "id": "VQ3BrFxltGDP" 753 | }, 754 | "source": [ 755 | "Note that the definition of the volume form is already in the `mlg.lbfgs` module. Also note that the standard L-BFGS does not support multi-batch training. You can still batch the dataset in case the GPU is out of memory, but the parameters are only updated after a whole epoch." 756 | ] 757 | }, 758 | { 759 | "cell_type": "markdown", 760 | "metadata": { 761 | "id": "P_sNfX12tGDQ" 762 | }, 763 | "source": [ 764 | "You can also check the test dataset:" 765 | ] 766 | }, 767 | { 768 | "cell_type": "code", 769 | "execution_count": null, 770 | "metadata": { 771 | "id": "pjRWkGZ4tGDQ", 772 | "outputId": "273e4c93-53d9-4783-84f0-3e940aab0bbd", 773 | "colab": { 774 | "base_uri": "https://localhost:8080/" 775 | } 776 | }, 777 | "outputs": [ 778 | { 779 | "output_type": "stream", 780 | "name": "stdout", 781 | "text": [ 782 | "sigma_test = 0.01207\n", 783 | "E_test = 0.00027\n" 784 | ] 785 | } 786 | ], 787 | "source": [ 788 | "sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE)\n", 789 | "E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE)\n", 790 | "print(\"sigma_test = %.5f\" % sigma_test)\n", 791 | "print(\"E_test = %.5f\" % E_test)" 792 | ] 793 | }, 794 | { 795 | "cell_type": "markdown", 796 | "metadata": { 797 | "id": "Xf6Dy8NvtGDQ" 798 | }, 799 | "source": [ 800 | "#### Print out the metrics\n", 801 | "After all of the trainings are done, the final results for the metrics can be printed out explicitly, using the previously generated data points and restriction matrices:" 802 | ] 803 | }, 804 | { 805 | "cell_type": "code", 806 | "execution_count": null, 807 | "metadata": { 808 | "id": "qt2ZZElptGDR" 809 | }, 810 | "outputs": [], 811 | "source": [ 812 | "@tf.function\n", 813 | "def get_cy_metric(points, restriction):\n", 814 | "\n", 815 | " cy_metric = mlg.complex_math.complex_hessian(tf.math.real(model(points)), points)\n", 816 | " cy_metric = tf.matmul(restriction, tf.matmul(cy_metric, restriction, adjoint_b=True))\n", 817 | "\n", 818 | " return cy_metric" 819 | ] 820 | }, 821 | { 822 | "cell_type": "code", 823 | "execution_count": null, 824 | "metadata": { 825 | "id": "FMOBAaCytGDR", 826 | "outputId": "8f40f4e8-cca9-4072-c9b5-b3e2236db0c0", 827 | "colab": { 828 | "base_uri": "https://localhost:8080/" 829 | } 830 | }, 831 | "outputs": [ 832 | { 833 | "output_type": "stream", 834 | "name": "stdout", 835 | "text": [ 836 | "[-0.59150815+7.2931312e-02j -0.9938062 +1.1493446e-02j\n", 837 | " 0.67252195-1.9070959e-01j -0.12152821+2.9399461e-01j\n", 838 | " 1. +2.2154981e-17j]\n", 839 | "[[ 2.472322 -4.78886477e-08j 0.3180696 +2.02831551e-01j\n", 840 | " -0.01433054+4.51943465e-02j]\n", 841 | " [ 0.31806967-2.02831566e-01j 2.557338 +7.45058060e-08j\n", 842 | " 0.02845549-1.64638966e-01j]\n", 843 | " [-0.01433052-4.51943949e-02j 0.02845548+1.64638966e-01j\n", 844 | " 1.9045304 -1.21071935e-08j]]\n" 845 | ] 846 | } 847 | ], 848 | "source": [ 849 | "cy_metric = get_cy_metric(points, restriction)\n", 850 | "print(points[5].numpy())\n", 851 | "print(cy_metric[5].numpy())" 852 | ] 853 | }, 854 | { 855 | "cell_type": "markdown", 856 | "metadata": { 857 | "id": "KeB5YqMHtGDR" 858 | }, 859 | "source": [ 860 | "### $\\eta$ on the rational curve" 861 | ] 862 | }, 863 | { 864 | "cell_type": "markdown", 865 | "metadata": { 866 | "id": "ZO-bZ-FstGDa" 867 | }, 868 | "source": [ 869 | "Now let's retrict our model to a subspace and check the local behavior of $\\eta$. With the quintic 3-fold f = 0, we can choose the embedding\n", 870 | "\n", 871 | "$$(z_0, -z_0, z_1, 0, -z_1),$$\n", 872 | "\n", 873 | "and the local coordinate system defined by $t = z_1 / z_0$. Using shperical coordinates $(\\theta, \\phi)$, it can be embedded into $\\mathbb{R}^3$ by:\n", 874 | "\n", 875 | "$$z_0 = \\sin \\theta \\cos \\phi, \\qquad z_1= \\sin \\theta \\sin \\phi + i \\cos \\phi$$\n", 876 | "\n", 877 | "So first sample the points on the rational curve:" 878 | ] 879 | }, 880 | { 881 | "cell_type": "code", 882 | "execution_count": null, 883 | "metadata": { 884 | "id": "3FtODaYEtGDa" 885 | }, 886 | "outputs": [], 887 | "source": [ 888 | "theta, phi = np.linspace(0.001,np.pi+0.001, 400), np.linspace(0.001, 2*np.pi+0.001, 400)\n", 889 | "eps = 0.0001 + 0.0001j\n", 890 | "\n", 891 | "R = []\n", 892 | "points_list = []\n", 893 | "for j in phi:\n", 894 | " for i in theta:\n", 895 | " t = complex(math.sin(i)*math.sin(j), math.cos(i)) / (math.sin(i)*math.cos(j))\n", 896 | " if np.absolute(t) <= 1:\n", 897 | " # The Bihomogeneous layer will remove the zero entries automatically.\n", 898 | " # So here we add a small number eps to avoid being removed\n", 899 | " points_list.append([1+eps, -1+eps, t+eps, 0+eps, -t+eps])\n", 900 | " else:\n", 901 | " # Use the symmetry:\n", 902 | " points_list.append([1+eps, -1+eps, 1/t+eps, 0+eps, -1/t+eps])" 903 | ] 904 | }, 905 | { 906 | "cell_type": "markdown", 907 | "metadata": { 908 | "id": "1jMeM3K3tGDb" 909 | }, 910 | "source": [ 911 | "Use this set of points to generate the rational curve with norm_coordinate = z0 and max_grad_coordinate = z1:" 912 | ] 913 | }, 914 | { 915 | "cell_type": "code", 916 | "execution_count": null, 917 | "metadata": { 918 | "id": "TDq6-gWwtGDb" 919 | }, 920 | "outputs": [], 921 | "source": [ 922 | "rc = mlg.hypersurface.Hypersurface(Z, f, points=points_list, norm_coordinate=0, max_grad_coordinate=0)\n", 923 | "rc_dataset = mlg.tf_dataset.generate_dataset(rc).batch(rc.n_points)" 924 | ] 925 | }, 926 | { 927 | "cell_type": "markdown", 928 | "metadata": { 929 | "id": "6--X-YkMtGDb" 930 | }, 931 | "source": [ 932 | "Calculate $\\eta$:" 933 | ] 934 | }, 935 | { 936 | "cell_type": "code", 937 | "execution_count": null, 938 | "metadata": { 939 | "id": "I2ypMTBmtGDb" 940 | }, 941 | "outputs": [], 942 | "source": [ 943 | "points, Omega_Omegabar, mass, restriction = next(iter(rc_dataset))\n", 944 | "det_omega = volume_form(points, Omega_Omegabar, mass, restriction)\n", 945 | "eta = (det_omega / Omega_Omegabar).numpy()" 946 | ] 947 | }, 948 | { 949 | "cell_type": "markdown", 950 | "metadata": { 951 | "id": "Qp0EdTwLtGDc" 952 | }, 953 | "source": [ 954 | "Convert to Cartesian coordinates:" 955 | ] 956 | }, 957 | { 958 | "cell_type": "code", 959 | "execution_count": null, 960 | "metadata": { 961 | "id": "VMECj1VotGDc" 962 | }, 963 | "outputs": [], 964 | "source": [ 965 | "R = eta.reshape(400, 400)\n", 966 | "THETA, PHI = np.meshgrid(theta, phi)\n", 967 | "X = R * np.sin(THETA) * np.cos(PHI)\n", 968 | "Y = R * np.sin(THETA) * np.sin(PHI)\n", 969 | "ZZ = R * np.cos(THETA)" 970 | ] 971 | }, 972 | { 973 | "cell_type": "markdown", 974 | "metadata": { 975 | "id": "KpygjzVRtGDc" 976 | }, 977 | "source": [ 978 | "Plot the figure:" 979 | ] 980 | }, 981 | { 982 | "cell_type": "code", 983 | "execution_count": null, 984 | "metadata": { 985 | "id": "A1E03PN8tGDc", 986 | "outputId": "e72b77a5-1b80-4e81-f394-c9ff255ea7ef", 987 | "colab": { 988 | "base_uri": "https://localhost:8080/", 989 | "height": 410 990 | } 991 | }, 992 | "outputs": [ 993 | { 994 | "output_type": "display_data", 995 | "data": { 996 | "text/plain": [ 997 | "
" 998 | ], 999 | "image/png": "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\n" 1000 | }, 1001 | "metadata": {} 1002 | } 1003 | ], 1004 | "source": [ 1005 | "fig = plt.figure()\n", 1006 | "ax = fig.add_subplot(1,1,1, projection='3d')\n", 1007 | "ax.set_zlim3d(-1.0, 1.0)\n", 1008 | "plot = ax.plot_surface(\n", 1009 | " X, Y, ZZ, rstride=1, cstride=1, cmap=plt.cm.YlGnBu_r,\n", 1010 | " linewidth=0, antialiased=False)" 1011 | ] 1012 | }, 1013 | { 1014 | "cell_type": "markdown", 1015 | "metadata": { 1016 | "id": "PyvsITrMtGDc" 1017 | }, 1018 | "source": [ 1019 | "$\\eta$ is expected to approach the constant function 1 as k increases." 1020 | ] 1021 | } 1022 | ], 1023 | "metadata": { 1024 | "kernelspec": { 1025 | "display_name": "Python 3", 1026 | "name": "python3" 1027 | }, 1028 | "language_info": { 1029 | "name": "python" 1030 | }, 1031 | "colab": { 1032 | "provenance": [], 1033 | "gpuType": "T4" 1034 | }, 1035 | "accelerator": "GPU" 1036 | }, 1037 | "nbformat": 4, 1038 | "nbformat_minor": 0 1039 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael R. Douglas, Subramanian Lakshminarasimhan and Yidi Qi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Leaderboard.md: -------------------------------------------------------------------------------- 1 | # Leaderboard 2 | 3 | ## Work in progress 4 | 5 | Here we will give both leaderboard and a link to a script which does the following: 6 | 7 | Run 5 prechosen sample CYs and apply the following criteria: 8 | 1. No more than 2 hours time per run (we will run submissions on our cluster). 9 | 2. Do each run twice with two choices of random seed and keep the worse one. 10 | 3. All runs must meet some minimal accuracy (e.g. 1% MAPE). 11 | 4. Rank the qualifying entries by average log MAPE (over all samples). 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MLGeometry 2 | 3 | Higher dimensional computational geometry using machine learning software 4 | 5 | - Kahler geometry and Kahler-Einstein metrics 6 | 7 | More to come. 8 | 9 | ## Recent Changes 10 | 11 | MLGeometry has been updated to be compatible with the lastest version of TensorFlow and Keras 3, and it can now be installed directly from PyPI. If you prefer the older version, please check the 'Using and Older Version' section below. 12 | 13 | ## Installation 14 | 15 | ### Prerequisites 16 | 17 | MLGeometry requires Python 3.11 and TensorFlow (>=2.16). 18 | 19 | Install TensorFlow by following the official installation guide: [TensorFlow Installation](https://www.tensorflow.org/install). 20 | 21 | On Linux with GPU, TensorFlow can be installed by 22 | 23 | pip install 'tensorflow[and-cuda]' 24 | 25 | ### Installing MLGeometry 26 | 27 | You can install MLGeometry using one of the following methods: 28 | 29 | #### Via PyPI 30 | 31 | pip install MLGeometry-tf 32 | 33 | *Note: Use "MLGeometry-tf" with a suffix when installing via pip.* 34 | 35 | #### Directly from Github 36 | 37 | pip install git+https://github.com/yidiq7/MLGeometry.git 38 | 39 | #### Using an Older Version 40 | 41 | If you prefer to use an older version of MLGeometry based on Tensorflow 2.12 and Keras 2, you can check out the previous release (v1.1.0) here: [Version 1.1.0 Release](https://github.com/yidiq7/MLGeometry/releases/tag/v1.1.0). Follow the installation instructions provided in that release's documentation. The compatible versions of Python and CUDA can be found [here](https://www.tensorflow.org/install/source#gpu). 42 | 43 | 44 | ## [Sample jupyter notebook](https://github.com/yidiq7/MLGeometry/blob/main/Guide.ipynb) 45 | 46 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/yidiq7/MLGeometry/blob/main/Guide.ipynb) 47 | 48 | ## Citation 49 | 50 | You can find our paper on [arxiv](https://arxiv.org/abs/2012.04797) or [PMLR](https://proceedings.mlr.press/v145/douglas22a.html). 51 | If you find our paper or package useful in your research or project, please cite it as follows: 52 | 53 | ``` 54 | @InProceedings{pmlr-v145-douglas22a, 55 | title = {Numerical Calabi-Yau metrics from holomorphic networks}, 56 | author = {Douglas, Michael and Lakshminarasimhan, Subramanian and Qi, Yidi}, 57 | booktitle = {Proceedings of the 2nd Mathematical and Scientific Machine Learning Conference}, 58 | pages = {223--252}, 59 | year = {2022}, 60 | editor = {Bruna, Joan and Hesthaven, Jan and Zdeborova, Lenka}, 61 | volume = {145}, 62 | series = {Proceedings of Machine Learning Research}, 63 | month = {16--19 Aug}, 64 | publisher = {PMLR}, 65 | pdf = {https://proceedings.mlr.press/v145/douglas22a/douglas22a.pdf}, 66 | url = {https://proceedings.mlr.press/v145/douglas22a.html}, 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "MLGeometry-tf" 7 | readme = "README.md" 8 | description = "Higher dimensional computational geometry using machine learning software" 9 | requires-python = ">=3.11" 10 | version = "1.2.0" 11 | dependencies = [ 12 | "tensorflow-probability[tf]", 13 | "sympy", 14 | "matplotlib" 15 | ] 16 | license = { file = "LICENSE" } 17 | maintainers = [ 18 | {name = "Yidi Qi", email = "qiyidi2012@gmail.com"} 19 | ] 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/yidiq7/MLGeometry" 23 | Changelog = "https://github.com/yidiq7/MLGeometry/blob/master/CHANGELOG.md" 24 | 25 | [tool.setuptools] 26 | packages = ["MLGeometry"] 27 | package-dir = {"" = "src"} 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | "tensorflow-probability[tf]" 2 | sympy 3 | matplotlib 4 | -------------------------------------------------------------------------------- /src/MLGeometry/__init__.py: -------------------------------------------------------------------------------- 1 | from . import hypersurface 2 | from . import cicyhypersurface 3 | from . import bihomoNN 4 | from . import lbfgs 5 | from . import loss 6 | from . import tf_dataset 7 | from . import complex_math 8 | 9 | -------------------------------------------------------------------------------- /src/MLGeometry/bihomoNN.py: -------------------------------------------------------------------------------- 1 | import keras 2 | from keras import activations 3 | import numpy as np 4 | import tensorflow as tf 5 | 6 | __all__ = ['Bihomogeneous','Bihomogeneous_k2','Bihomogeneous_k3', 7 | 'Bihomogeneous_k4','SquareDense','WidthOneDense'] 8 | 9 | @keras.saving.register_keras_serializable(package="MLGeometry") 10 | class Bihomogeneous(keras.layers.Layer): 11 | '''A layer transform zi to zi*zjbar''' 12 | def __init__(self, d=5): 13 | super(Bihomogeneous, self).__init__() 14 | self.d = d 15 | 16 | def call(self, inputs): 17 | zzbar = tf.einsum('ai,aj->aij', inputs, tf.math.conj(inputs)) 18 | zzbar = tf.linalg.band_part(zzbar, 0, -1) 19 | zzbar = tf.reshape(zzbar, [-1, self.d**2]) 20 | zzbar = tf.concat([tf.math.real(zzbar), tf.math.imag(zzbar)], axis=1) 21 | return remove_zero_entries(zzbar) 22 | 23 | 24 | @keras.saving.register_keras_serializable(package="MLGeometry") 25 | class Bihomogeneous_k2(keras.layers.Layer): 26 | '''A layer transform zi to symmetrized zi1*zi2, then to zi1*zi2 * zi1zi2bar''' 27 | def __init__(self): 28 | super(Bihomogeneous_k2, self).__init__() 29 | 30 | def call(self, inputs): 31 | # zi to zi1*zi2 32 | zz = tf.einsum('ai,aj->aij', inputs, inputs) 33 | zz = tf.linalg.band_part(zz, 0, -1) # zero below upper triangular 34 | zz = tf.reshape(zz, [-1, 5**2]) 35 | zz = tf.reshape(remove_zero_entries(zz), [-1, 15]) 36 | 37 | # zi1*zi2 to zzbar 38 | zzbar = tf.einsum('ai,aj->aij', zz, tf.math.conj(zz)) 39 | zzbar = tf.linalg.band_part(zzbar, 0, -1) 40 | zzbar = tf.reshape(zzbar, [-1, 15**2]) 41 | zzbar = tf.concat([tf.math.real(zzbar), tf.math.imag(zzbar)], axis=1) 42 | return remove_zero_entries(zzbar) 43 | 44 | 45 | @keras.saving.register_keras_serializable(package="MLGeometry") 46 | class Bihomogeneous_k3(keras.layers.Layer): 47 | '''A layer transform zi to symmetrized zi1*zi2*zi3, then to zzbar''' 48 | def __init__(self): 49 | super(Bihomogeneous_k3, self).__init__() 50 | 51 | def call(self, inputs): 52 | zz = tf.einsum('ai,aj,ak->aijk', inputs, inputs, inputs) 53 | zz = tf.linalg.band_part(zz, 0, -1) # keep upper triangular 2/3 54 | zz = tf.transpose(zz, perm=[0, 3, 1, 2]) 55 | zz = tf.linalg.band_part(zz, 0, -1) # keep upper triangular 1/2 56 | zz = tf.transpose(zz, perm=[0, 2, 3, 1]) 57 | zz = tf.reshape(zz, [-1, 5**3]) 58 | zz = tf.reshape(remove_zero_entries(zz), [-1, 35]) 59 | 60 | zzbar = tf.einsum('ai,aj->aij', zz, tf.math.conj(zz)) 61 | zzbar = tf.linalg.band_part(zzbar, 0, -1) 62 | zzbar = tf.reshape(zzbar, [-1, 35**2]) 63 | zzbar = tf.concat([tf.math.real(zzbar), tf.math.imag(zzbar)], axis=1) 64 | return remove_zero_entries(zzbar) 65 | 66 | 67 | @keras.saving.register_keras_serializable(package="MLGeometry") 68 | class Bihomogeneous_k4(keras.layers.Layer): 69 | '''A layer transform zi to symmetrized zi1*zi2*zi3*zi4, then to zzbar''' 70 | def __init__(self): 71 | super(Bihomogeneous_k4, self).__init__() 72 | 73 | def call(self, inputs): 74 | zz = tf.einsum('ai,aj,ak,al->aijkl', inputs, inputs, inputs, inputs) 75 | zz = tf.linalg.band_part(zz, 0, -1) 76 | zz = tf.transpose(zz, perm=[0, 4, 1, 2, 3]) 77 | zz = tf.linalg.band_part(zz, 0, -1) 78 | zz = tf.transpose(zz, perm=[0, 4, 1, 2, 3]) # 3412 79 | zz = tf.linalg.band_part(zz, 0, -1) 80 | zz = tf.reshape(zz, [-1, 5**4]) 81 | zz = tf.reshape(remove_zero_entries(zz), [-1, 70]) 82 | 83 | zzbar = tf.einsum('ai,aj->aij', zz, tf.math.conj(zz)) 84 | zzbar = tf.linalg.band_part(zzbar, 0, -1) 85 | zzbar = tf.reshape(zzbar, [-1, 70**2]) 86 | zzbar = tf.concat([tf.math.real(zzbar), tf.math.imag(zzbar)], axis=1) 87 | return remove_zero_entries(zzbar) 88 | 89 | 90 | def remove_zero_entries(x): 91 | x = tf.transpose(x) 92 | intermediate_tensor = tf.reduce_sum(tf.abs(x), 1) 93 | bool_mask = tf.squeeze(tf.math.logical_not(tf.math.less(intermediate_tensor, 1e-3))) 94 | x = tf.boolean_mask(x, bool_mask) 95 | x = tf.transpose(x) 96 | return x 97 | 98 | 99 | @keras.saving.register_keras_serializable(package="MLGeometry") 100 | class SquareDense(keras.layers.Layer): 101 | def __init__(self, input_dim, units, activation=tf.square, trainable=True): 102 | super(SquareDense, self).__init__() 103 | w_init = tf.random_normal_initializer(mean=0.0, stddev=0.05) 104 | self.w = self.add_weight( 105 | shape=(input_dim, units), 106 | initializer=keras.initializers.Constant( 107 | tf.math.abs(w_init(shape=(input_dim, units), dtype='float32'))), 108 | #initial_value=w_init(shape=(input_dim, units), dtype='float32'), 109 | trainable=trainable, 110 | ) 111 | self.activation = activations.get(activation) 112 | 113 | def call(self, inputs): 114 | return self.activation(tf.matmul(inputs, self.w)) 115 | 116 | 117 | @keras.saving.register_keras_serializable(package="MLGeometry") 118 | class WidthOneDense(keras.layers.Layer): 119 | ''' 120 | Usage: layer = WidthOneDense(n**2, 1) 121 | where n is the number of sections for different ks 122 | n = 5 for k = 1 123 | n = 15 for k = 2 124 | n = 35 for k = 3 125 | This layer is used directly after Bihomogeneous_k layers to sum over all 126 | the terms in the previous layer. The weights are initialized so that the h 127 | matrix is a real identity matrix. The training does not work if they are randomly 128 | initialized. 129 | ''' 130 | def __init__(self, input_dim, units, activation=None, trainable=True): 131 | super(WidthOneDense, self).__init__() 132 | dim = int(np.sqrt(input_dim)) 133 | mask = tf.cast(tf.linalg.band_part(tf.ones([dim, dim]),0,-1), dtype=tf.bool) 134 | upper_tri = tf.boolean_mask(tf.eye(dim), mask) 135 | w_init = tf.reshape(tf.concat([upper_tri, tf.zeros(input_dim - len(upper_tri))], axis=0), [-1, 1]) 136 | self.w = self.add_weight( 137 | shape=(input_dim, units), 138 | initializer=keras.initializer(w_init), 139 | trainable=trainable, 140 | ) 141 | self.activation = activations.get(activation) 142 | 143 | def call(self, inputs): 144 | return self.activation(tf.matmul(inputs, self.w)) 145 | 146 | -------------------------------------------------------------------------------- /src/MLGeometry/cicyhypersurface.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | import sympy as sp 4 | #import tensorflow as tf 5 | import mpmath 6 | from multiprocessing import Pool 7 | from .hypersurface import Hypersurface 8 | from .hypersurface import RealHypersurface 9 | 10 | __all__ = ['RealHypersurface', 'CICYRealHypersurface'] 11 | 12 | class CICYHypersurface(Hypersurface): 13 | 14 | def solve_points(self, n_trios): 15 | points = [] 16 | ztrios = self.generate_random_projective(n_trios, 3) 17 | coeff_a = sp.var('a0:{}'.format(self.n_dim)) 18 | coeff_b = sp.var('b0:{}'.format(self.n_dim)) 19 | coeff_c = sp.var('c0:{}'.format(self.n_dim)) 20 | sp.var('t0:2') 21 | coeff_zip = zip(coeff_a, coeff_b, coeff_c) 22 | plane = [t0*a + t1*b + c for (a, b, c) in coeff_zip] 23 | # Add another function & poly & coeffs here 24 | poly_t = sp.Matrix(self.function).subs([(self.coordinates[i], plane[i]) 25 | for i in range(self.n_dim)]) 26 | 27 | coeffs_list = [sp.Poly(poly,(t0, t1)).coeffs() for poly in poly_t] 28 | monoms_list = [sp.Poly(poly,(t0, t1)).monoms() for poly in poly_t] 29 | 30 | coeffs_func = sp.lambdify([coeff_a + coeff_b + coeff_c], coeffs_list, "numpy") 31 | 32 | coeffs_list = [] 33 | for ztrio in ztrios: 34 | coeffs_list.append(coeffs_func(np.array(ztrio).flatten())) 35 | 36 | monoms_list = [monoms_list] * n_trios 37 | 38 | points = self.solve_points_multiprocessing(coeffs_list, monoms_list, ztrios) 39 | 40 | return points 41 | 42 | def solve_points_multiprocessing(self, coeffs_list, monoms_list, ztrios): 43 | points = [] 44 | with Pool() as pool: 45 | for point in pool.starmap(CICYHypersurface.solve_poly, zip(coeffs_list, monoms_list, ztrios)): 46 | points.append(point) 47 | 48 | points = list(filter(lambda x: x is not None, points)) 49 | return points 50 | 51 | @staticmethod 52 | def solve_poly(coeff_list, monom_list, ztrio): 53 | point = None 54 | 55 | def func_t(t0, t1): 56 | return [sum([coeff * t0**monom[0]*t1**monom[1] for coeff, monom in zip(coeffs, monoms)]) for coeffs, monoms in zip(coeff_list, monom_list)] 57 | 58 | for attempt in range(1): 59 | try: 60 | t_real = np.random.randn(4) 61 | t_init = [complex(t_real[0], t_real[1]), complex(t_real[2], t_real[3])] 62 | #t_init = np.random.randn(2).tolist() 63 | t_solved = mpmath.findroot(func_t, t_init) 64 | t_array = np.array(t_solved.tolist(), dtype=np.complex64) 65 | t_array = np.concatenate((t_array, np.array([[1.0+0.0j]]))) 66 | point = np.add.reduce(t_array * ztrio) 67 | break 68 | except: 69 | pass 70 | 71 | return point 72 | 73 | def get_grad(self): 74 | func = sp.Matrix(self.function) 75 | grad = func.jacobian(self.affine_coordinates) 76 | return grad 77 | 78 | def get_hol_n_form(self, coord): 79 | """ 80 | 81 | Return: 82 | ------- 83 | A or a list of symbolic expressions of the holomorphic n-form 1/(∂f/∂z_i) 84 | 85 | """ 86 | hol_n_form = [] 87 | try: 88 | hol_n_form = 1/self.grad[:,coord].det() 89 | except: 90 | logging.exception('The number of functions and the number of coordinates to eliminate do not match') 91 | 92 | return hol_n_form 93 | 94 | def autopatch(self): 95 | # projective patches 96 | points_on_patch = [[] for i in range(self.n_dim)] 97 | for point in self.points: 98 | norms = np.absolute(point) 99 | for i in range(self.n_dim): 100 | if norms[i] == max(norms): 101 | point_normalized = self.normalize_point(point, i) 102 | points_on_patch[i].append(point_normalized) 103 | continue 104 | for i in range(self.n_dim): 105 | self.set_patch(points_on_patch[i], i) 106 | 107 | # Remove empty patches 108 | self.patches = [subpatch for subpatch in self.patches if subpatch.points] 109 | 110 | for patch in self.patches: 111 | 112 | jac_det = [] 113 | for i in range(self.n_dim-1): 114 | det_row = [] 115 | for j in range(self.n_dim-1): 116 | det_row.append(patch.grad[:, [i,j]].det()) 117 | jac_det.append(det_row) 118 | 119 | jac_det = sp.Matrix(jac_det) 120 | jac_det = sp.lambdify([self.coordinates], jac_det, 'numpy') 121 | 122 | jac_det_arr = np.abs(np.squeeze(np.vectorize(jac_det,signature='(n)->(p,q)')(patch.points))) 123 | 124 | n, m, _ = jac_det_arr.shape 125 | # Reshape the array to a 2D array where each row represents one mxm subarray 126 | reshaped_arr = jac_det_arr.reshape(n, -1) 127 | # Find the argmax indices for each row (mxm subarray) 128 | argmax_indices = np.argmax(reshaped_arr, axis=1) 129 | # Convert the flat indices to row and column indices 130 | row_indices, col_indices = np.unravel_index(argmax_indices, (m, m)) 131 | # Stack the row and column indices horizontally to get the final result 132 | result = np.column_stack((row_indices, col_indices)) 133 | 134 | max_grad_list = np.unique(result, axis=0).tolist() 135 | 136 | points_arr = np.array(patch.points) 137 | for max_grad_coord in max_grad_list: 138 | points_on_patch = points_arr[np.where(np.all(result == max_grad_coord, axis=1))] 139 | patch.set_patch(points_on_patch, patch.norm_coordinate, max_grad_coord=max_grad_coord) 140 | 141 | def set_patch(self, points_on_patch, norm_coord=None, max_grad_coord=None): 142 | new_patch = CICYHypersurface(self.coordinates, 143 | self.function, 144 | points=points_on_patch, 145 | norm_coordinate=norm_coord, 146 | max_grad_coordinate=max_grad_coord) 147 | self.patches.append(new_patch) 148 | 149 | def get_restriction(self, ignored_coord=None, lambdify=False): 150 | if ignored_coord is None: 151 | ignored_coord = self.max_grad_coordinate 152 | # Since we have more than one ignored_coordinate in CICY, sympy subs() 153 | # cannot replace two coordinates simultaneously. As a result, if the first 154 | # expression contains the coordinate to be replaced by the second expression 155 | # that coordinate will also be replaced. So here we will create a temporary 156 | # coordinate list W to avoid this issue 157 | W = sp.var('w0:{}'.format(len(self.affine_coordinates))) 158 | ignored_coordinate = np.array(W)[ignored_coord] 159 | local_coordinates = sp.Matrix(W).subs({coord: func for coord, func in zip(ignored_coordinate, self.function)}) 160 | local_coordinates = local_coordinates.subs({w: z for w, z in zip(W, self.affine_coordinates)}) 161 | restriction = local_coordinates.jacobian(self.affine_coordinates).inv() 162 | for coord in reversed(ignored_coord): 163 | restriction.col_del(coord) 164 | if lambdify is True: 165 | restriction = sp.lambdify([self.coordinates], restriction, 'numpy') 166 | return restriction 167 | 168 | class RealCICYHypersurface(CICYHypersurface, RealHypersurface): 169 | 170 | def generate_random_projective(self, n_set, n_pt_in_a_set): 171 | return RealHypersurface.generate_random_projective(self, n_set, n_pt_in_a_set) 172 | 173 | def solve_points_multiprocessing(self, coeffs_list, monoms_list, ztrios): 174 | points = [] 175 | with Pool() as pool: 176 | for point in pool.starmap(RealCICYHypersurface.solve_poly, zip(coeffs_list, monoms_list, ztrios)): 177 | points.append(point) 178 | 179 | points = list(filter(lambda x: x is not None, points)) 180 | return points 181 | 182 | @staticmethod 183 | def solve_poly(coeff_list, monom_list, ztrio): 184 | point = None 185 | 186 | def func_t(t0, t1): 187 | return [sum([coeff * t0**monom[0]*t1**monom[1] for coeff, monom in zip(coeffs, monoms)]) for coeffs, monoms in zip(coeff_list, monom_list)] 188 | 189 | for attempt in range(1): 190 | try: 191 | #t_real = np.random.randn(4) 192 | #t_init = [complex(t_real[0], t_real[1]), complex(t_real[2], t_real[3])] 193 | t_init = np.random.randn(2).tolist() 194 | t_solved = mpmath.findroot(func_t, t_init) 195 | t_array = np.array(t_solved.tolist(), dtype=np.complex64) 196 | t_array = np.concatenate((t_array, np.array([[1.0+0.0j]]))) 197 | point = np.add.reduce(t_array * ztrio) 198 | break 199 | except: 200 | pass 201 | 202 | return point 203 | -------------------------------------------------------------------------------- /src/MLGeometry/complex_math.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | 3 | def gradients_zbar(func, x): 4 | dx_real = tf.gradients(tf.math.real(func), x) 5 | dx_imag = tf.gradients(tf.math.imag(func), x) 6 | return (dx_real + dx_imag*tf.constant(1j, dtype=x.dtype)) / 2 7 | 8 | @tf.autograph.experimental.do_not_convert 9 | def complex_hessian(func, x): 10 | # Take a real function and calculate dzdzbar(f) 11 | #grad = gradients_z(func, x) 12 | grad = tf.math.conj(tf.gradients(func, x)) 13 | hessian = tf.stack([gradients_zbar(tmp[0], x)[0] 14 | for tmp in tf.unstack(grad, axis=2)], 15 | axis = 1) / 2.0 16 | 17 | return hessian 18 | 19 | -------------------------------------------------------------------------------- /src/MLGeometry/hypersurface.py: -------------------------------------------------------------------------------- 1 | """Defines a Python class for hypersurfaces""" 2 | 3 | from multiprocessing import Pool 4 | import mpmath 5 | 6 | import numpy as np 7 | import sympy as sp 8 | import tensorflow as tf 9 | 10 | __all__ = ['Hypersurface', 'RealHypersurface', 'diff', 'diff_conjugate'] 11 | 12 | class Hypersurface(): 13 | r"""A hypersurface or patch defined both symbolically and numerically. 14 | 15 | The Hypersuface class contains the symbolic polynomial representation of a 16 | hypersurface in sympy. It is also numerically defined as a collection of 17 | points on the hypersurface. The points are sperated into patches, which are 18 | also collectons of points. Therefore, recursively, the patches can also be 19 | defined as instances of the Hypersurface class. 20 | 21 | Hypersurface 22 | / | \ 23 | patch1 patch2 patch3 (Also Hypersurface class) 24 | / | \ / | \ / | \ 25 | subpatch .. .. .. .. .. .. 26 | 27 | Attribute: 28 | ------------- 29 | coordinates: 30 | The homogeneous coordinates as a list of sympy symbols, e.g. 31 | z0, z1, z2, z3, z4 = sp.symbols('z0, z1, z2, z3, z4') 32 | Z = [z0, z2, z3, z3, z4] 33 | function: 34 | A function of the homogeneous coordiantes, e.g. 35 | f = z0**5 + z1**5 + z2**5 + z3**5 + z4**5 + 0.5*z0*z1*z2*z3*z4 36 | The hypersurface is defined by f = 0 37 | n_dim: The number of homogeneous coordiantes. 38 | norm_coordinate: 39 | Applicable if the instance is a patch. An integer representing the 40 | index of the coordinate set to 1 on the affine patch. The first level 41 | of patches are defined with this coordinate. The corresponding symbolic 42 | coordinate is self.coordiante[self.norm_coordiante]. 43 | affine_coordinates: 44 | The coordiantes on the affine patches (withouth the norm_coordinate). 45 | max_grad_coordinate: 46 | The index of the affine coordinate with the largest |∂f/∂z_i|. The 47 | second level of patches (subpatches) are defined using this coordinate, 48 | so that when one computes the holomorphic n-form 49 | Omega = 1/(∂f/∂z_i) * (dz_1 ^ ... dz_{i-1} ^ dz_{i+1} ^ ... dz_N), 50 | it is less likely to have a small number on the denominator. 51 | The corresponding symbolic coordinate is 52 | self.affine_coordinate[self.max_grad_coordinate]. 53 | patches: A list of instances of the subpatches of the hypersurface/patch 54 | points: The numerical points generated by Monte Carlo. 55 | n_points: The total number of points on the hypersurface or a patch. 56 | grad: A list of symbolic expressions of ∂f/∂z_i 57 | 58 | Usage: 59 | ---------- 60 | Firstly, one needs to define the coordinates and function with sympy, 61 | and the number of pairs of points used in Monte Carlo: 62 | 63 | z0, z1, z2, z3, z4 = sp.symbols('z0, z1, z2, z3, z4') 64 | Z = [z0, z2, z3, z3, z4] 65 | f = z0**5 + z1**5 + z2**5 + z3**5 + z4**5 + 0.5*z0*z1*z2*z3*z4 66 | n_pairs = 1000 67 | 68 | Then define the hypersurface with f = 0: 69 | 70 | HS = MLGeometry.hypersurface.Hypersurface(Z, f, npairs) 71 | 72 | """ 73 | def __init__(self, 74 | coordinates, 75 | function, 76 | n_pairs=0, 77 | points=None, 78 | norm_coordinate=None, 79 | max_grad_coordinate=None): 80 | """Initialize the hypersurface 81 | 82 | Given the sympy coordinates Z, function f and npairs, there are two 83 | main steps on the highest level: 84 | 1. Generate points using Monte Carlo methods with __solve_points() 85 | 2. Define the patches automatically with autopatch() 86 | On the patches, the points are calculated with autopatch() 87 | beforehand and passed as an argument. 88 | 89 | """ 90 | self.coordinates = np.array(coordinates) 91 | self.function = function 92 | self.n_dim = len(self.coordinates) 93 | self.norm_coordinate = norm_coordinate 94 | if norm_coordinate is not None: 95 | self.affine_coordinates = np.delete(self.coordinates, norm_coordinate) 96 | else: 97 | self.affine_coordinates = self.coordinates 98 | self.max_grad_coordinate = max_grad_coordinate 99 | self.patches = [] 100 | if points is None: 101 | self.points = self.solve_points(n_pairs) 102 | self.autopatch() 103 | else: 104 | self.points = points 105 | #self.n_patches = len(self.patches) 106 | self.n_points = len(self.points) 107 | self.grad = self.get_grad() 108 | 109 | def solve_points(self, n_pairs): 110 | """Generates random points on the hypersurface with Monte Carlo 111 | 112 | #TODO explain the MC method or refer to the paper 113 | 114 | Args: 115 | -------- 116 | n_pairs: The number of random pair used in Monte carlo. 117 | 118 | Returns: 119 | -------- 120 | A list of random complex points on the hypersurface, where the points 121 | themselves are a list of complex coordiantes with dtype Complex128. 122 | 123 | """ 124 | zpairs = self.generate_random_projective(n_pairs, 2) 125 | coeff_a = [sp.symbols('a'+str(i)) for i in range(self.n_dim)] 126 | coeff_b = [sp.symbols('b'+str(i)) for i in range(self.n_dim)] 127 | c = sp.symbols('c') 128 | coeff_zip = zip(coeff_a, coeff_b) 129 | line = [c*a+b for (a, b) in coeff_zip] 130 | function_eval = self.function.subs([(self.coordinates[i], line[i]) 131 | for i in range(self.n_dim)]) 132 | poly = sp.Poly(function_eval, c) 133 | coeff_poly = poly.coeffs() 134 | get_coeff = sp.lambdify([coeff_a, coeff_b], coeff_poly) 135 | 136 | points = self.solve_points_multiprocessing(zpairs, get_coeff) 137 | 138 | return points 139 | 140 | def solve_points_multiprocessing(self, zpairs, get_coeff): 141 | points = [] 142 | # Multiprocessing. Then append the points to the same list in the main process 143 | with Pool() as pool: 144 | for points_d in pool.starmap(Hypersurface.solve_poly, 145 | zip(zpairs, [get_coeff(zpair[0], zpair[1]) 146 | for zpair in zpairs])): 147 | points.extend(points_d) 148 | return points 149 | 150 | def generate_random_projective(self, n_set, n_pt_in_a_set): 151 | """Generate sets of points in CP^N 152 | 153 | Args: 154 | -------- 155 | n_set: The total number of sets/complex lines/planes sampled. Equivalent 156 | to n_pairs when there are 2 points in each set. 157 | 158 | n_pt_in_a_set: The number of points in a set. For pairs it equals to 2 and 159 | for trios it equal to 3, etc. 160 | 161 | Returns: 162 | -------- 163 | A list of random points in (CP^N)^n_point 164 | 165 | """ 166 | z_random = [] 167 | for i in range(n_set): 168 | zv = [] 169 | for j in range(n_pt_in_a_set): 170 | zv.append([complex(c[0],c[1]) for c in np.random.normal(0.0, 1.0, (self.n_dim, 2))]) 171 | z_random.append(zv) 172 | return z_random 173 | 174 | @staticmethod 175 | def solve_poly(zpair, coeff): 176 | # For each zpair there are d solutions, where d is the n_dim 177 | points_d = [] 178 | c_solved = mpmath.polyroots(coeff) 179 | for pram_c in c_solved: 180 | points_d.append([complex(pram_c * a + b) 181 | for (a, b) in zip(zpair[0], zpair[1])]) 182 | return points_d 183 | 184 | def autopatch(self): 185 | # projective patches 186 | points_on_patch = [[] for i in range(self.n_dim)] 187 | for point in self.points: 188 | norms = np.absolute(point) 189 | for i in range(self.n_dim): 190 | if norms[i] == max(norms): 191 | point_normalized = self.normalize_point(point, i) 192 | points_on_patch[i].append(point_normalized) 193 | continue 194 | for i in range(self.n_dim): 195 | if points_on_patch[i]: 196 | self.set_patch(points_on_patch[i], i) 197 | # Subpatches on each patch 198 | for patch in self.patches: 199 | points_on_patch = [[] for i in range(self.n_dim-1)] 200 | grad_eval = sp.lambdify(self.coordinates, patch.grad, 'numpy') 201 | for point in patch.points: 202 | grad = grad_eval(*point) 203 | grad_norm = np.absolute(grad) 204 | points_on_patch[np.argmax(grad_norm)].append(point) 205 | for i in range(self.n_dim-1): 206 | if points_on_patch[i]: 207 | patch.set_patch(points_on_patch[i], patch.norm_coordinate, 208 | max_grad_coord=i) 209 | 210 | def set_patch(self, points_on_patch, norm_coord=None, max_grad_coord=None): 211 | new_patch = Hypersurface(self.coordinates, 212 | self.function, 213 | points=points_on_patch, 214 | norm_coordinate=norm_coord, 215 | max_grad_coordinate=max_grad_coord) 216 | self.patches.append(new_patch) 217 | 218 | def list_patches(self): 219 | print("Number of Patches:", len(self.patches)) 220 | i = 1 221 | for patch in self.patches: 222 | print("Points on patch", i, ":", len(patch.points)) 223 | i = i + 1 224 | 225 | def normalize_point(self, point, norm_coordinate): 226 | point_normalized = [] 227 | for coordinate in point: 228 | norm_coefficient = point[norm_coordinate] 229 | coordinate_normalized = coordinate / norm_coefficient 230 | point_normalized.append(coordinate_normalized) 231 | return point_normalized 232 | 233 | def get_FS(self): 234 | FS_metric = self.kahler_metric(np.identity(self.n_dim), k=1) 235 | return FS_metric 236 | 237 | def get_grad(self): 238 | grad = [] 239 | for coord in self.affine_coordinates: 240 | grad_i = self.function.diff(coord) 241 | grad.append(grad_i) 242 | return grad 243 | 244 | def get_hol_n_form(self, coord): 245 | """ 246 | 247 | Return: 248 | ------- 249 | A or a list of symbolic expressions of the holomorphic n-form 1/(∂f/∂z_i) 250 | 251 | """ 252 | hol_n_form = [] 253 | if coord is not None: 254 | hol_n_form = 1 / self.grad[coord] 255 | else: 256 | for i in range(len(self.affine_coordinates)): 257 | hol_n_form.append(self.get_hol_n_form(i)) 258 | return hol_n_form 259 | 260 | def get_omega_omegabar(self, lambdify=False): 261 | omega_omegabar = [] 262 | if self.patches == [] and self.max_grad_coordinate is not None: 263 | hol_n_form = self.get_hol_n_form(self.max_grad_coordinate) 264 | omega_omegabar = hol_n_form * sp.conjugate(hol_n_form) 265 | else: 266 | for patch in self.patches: 267 | try: 268 | omega_omegabar.append(patch.omega_omegabar) 269 | except AttributeError: 270 | omega_omegabar.append(patch.get_omega_omegabar(lambdify=lambdify)) 271 | 272 | if lambdify is True: 273 | OObar_func = sp.lambdify([self.coordinates], omega_omegabar,'numpy') 274 | omega_omegabar = lambda point: OObar_func(point).real 275 | return omega_omegabar 276 | 277 | def get_sections(self, k, lambdify=False): 278 | sections = [] 279 | t = sp.symbols('t') 280 | GenSec = sp.prod(1/(1-(t*zz)) for zz in self.coordinates) 281 | poly = sp.series(GenSec, t, n=k+1).coeff(t**k) 282 | while poly!=0: 283 | sections.append(sp.LT(poly)) 284 | poly = poly - sp.LT(poly) 285 | n_sections = len(sections) 286 | sections = np.array(sections) 287 | if lambdify is True: 288 | sections = sp.lambdify([self.coordinates], sections, 'numpy') 289 | return sections, n_sections 290 | 291 | def kahler_potential(self, h_matrix=None, k=1): 292 | sections, n_sec = self.get_sections(k) 293 | if h_matrix is None: 294 | h_matrix = np.identity(n_sec) 295 | # Check if h_matrix is a string 296 | elif isinstance(h_matrix, str): 297 | if h_matrix == "identity": 298 | h_matrix = np.identity(n_sec) 299 | elif h_matrix == "symbolic": 300 | h_matrix = sp.MatrixSymbol('H', n_sec, n_sec) 301 | 302 | elif h_matrix == "FS": 303 | h_matrix = np.diag(sp.Poly(sp.expand(sum(self.coordinates)**k)).coeffs()) 304 | z_H_zbar = np.matmul(sections, np.matmul(h_matrix, sp.conjugate(sections))) 305 | if self.norm_coordinate is not None: 306 | z_H_zbar = z_H_zbar.subs(self.coordinates[self.norm_coordinate], 1) 307 | kahler_potential = sp.log(z_H_zbar) 308 | return kahler_potential 309 | 310 | def kahler_metric(self, h_matrix=None, k=1, point=None): 311 | if point is None: 312 | pot = self.kahler_potential(h_matrix, k) 313 | metric = [] 314 | #i holomorphc, j anti-hol 315 | for coord_i in self.affine_coordinates: 316 | a_holo_der = [] 317 | for coord_j in self.affine_coordinates: 318 | a_holo_der.append(diff_conjugate(pot, coord_j)) 319 | metric.append([diff(ah, coord_i) for ah in a_holo_der]) 320 | metric = sp.Matrix(metric) 321 | 322 | return metric 323 | 324 | def get_restriction(self, ignored_coord=None, lambdify=False): 325 | if ignored_coord is None: 326 | ignored_coord = self.max_grad_coordinate 327 | ignored_coordinate = self.affine_coordinates[ignored_coord] 328 | local_coordinates = sp.Matrix(self.affine_coordinates).subs(ignored_coordinate, self.function) 329 | affine_coordinates = sp.Matrix(self.affine_coordinates) 330 | restriction = local_coordinates.jacobian(affine_coordinates).inv() 331 | restriction.col_del(ignored_coord) 332 | if lambdify is True: 333 | restriction = sp.lambdify([self.coordinates], restriction, 'numpy') 334 | return restriction 335 | 336 | def get_FS_volume_form(self, h_matrix=None, k=1, lambdify=False): 337 | kahler_metric = self.kahler_metric(h_matrix, k) 338 | restriction = self.get_restriction() 339 | FS_volume_form = restriction.T * kahler_metric * restriction.conjugate() 340 | FS_volume_form = FS_volume_form.det() 341 | if lambdify is True: 342 | FS_func = sp.lambdify([self.coordinates], FS_volume_form, 'numpy') 343 | FS_volume_form = lambda point: FS_func(point).real 344 | return FS_volume_form 345 | 346 | # Numerical Methods: 347 | 348 | def set_k(self, k): 349 | self.k = k 350 | sections, ns = self.get_sections(k, lambdify=False) 351 | sections_func, ns = self.get_sections(k, lambdify=True) 352 | self.n_sections = ns 353 | for patch in self.patches: 354 | # patch.k = k 355 | for subpatch in patch.patches: 356 | # subpatch.k = k 357 | subpatch.n_sections = ns 358 | subpatch.sections = sections_func 359 | jacobian = sp.Matrix(sections).jacobian(subpatch.affine_coordinates) 360 | subpatch.sections_jacobian = sp.lambdify([subpatch.coordinates], 361 | jacobian,'numpy') 362 | subpatch.restriction = subpatch.get_restriction(lambdify=True) 363 | subpatch.omega_omegabar = subpatch.get_omega_omegabar(lambdify=True) 364 | subpatch.h_FS = np.diag(sp.Poly(sp.expand(sum(self.coordinates)**k)).coeffs()) 365 | 366 | # Tensors 367 | subpatch.s_tf, subpatch.J_tf = subpatch.num_s_J_tf() 368 | subpatch.s_tf_1, subpatch.J_tf_1 = subpatch.num_s_J_tf(k=1) 369 | subpatch.Omega_Omegabar_tf = subpatch.num_Omega_Omegabar_tf() 370 | subpatch.r_tf = subpatch.num_restriction_tf() 371 | 372 | #@tf.function 373 | def num_s_J_tf(self, k=-1): 374 | 375 | s_vec = [] 376 | J_vec = [] 377 | 378 | for point in self.points: 379 | if k == 1: 380 | # k = 1 will be used in the mass formula during the integration 381 | s = [point] 382 | # Delete the correspoding row 383 | J = np.delete(np.identity(self.n_dim), self.norm_coordinate, 0) 384 | else: 385 | s = [self.sections(point)] 386 | J = self.sections_jacobian(point).T 387 | 388 | s_vec.append(s) 389 | J_vec.append(J) 390 | 391 | s_tf = tf.constant(np.array(s_vec, dtype=np.complex64)) 392 | J_tf = tf.constant(np.array(J_vec, dtype=np.complex64)) 393 | return s_tf, J_tf 394 | 395 | #@tf.function 396 | def num_Omega_Omegabar_tf(self): 397 | Omega_Omegabar = [] 398 | for point in self.points: 399 | Omega_Omegabar.append(self.omega_omegabar(point)) 400 | Omega_Omegabar = tf.constant(np.array(Omega_Omegabar, dtype=np.float32)) 401 | return Omega_Omegabar 402 | 403 | #@tf.function 404 | def num_restriction_tf(self): 405 | # Maybe I shouldn't do transpose here. A little bit confusing but 406 | # I guarantee you that the calculations are correct 407 | r = [] 408 | for point in self.points: 409 | r.append(self.restriction(point).T) 410 | r_tf = tf.constant(np.array(r, dtype=np.complex64)) 411 | return r_tf 412 | 413 | #@tf.function 414 | def num_kahler_metric_tf(self, h_matrix, k=-1): 415 | if isinstance(h_matrix, str): 416 | if h_matrix == 'identity': 417 | if k == 1: 418 | h_matrix = np.identity(self.n_dim, dtype=np.complex64) 419 | else: 420 | h_matrix = np.identity(self.n_sections, dtype=np.complex64) 421 | elif h_matrix == 'FS': 422 | h_matrix = self.h_FS.astype(np.complex64) 423 | #h_matrix = np.array(self.h_FS, dtype=np.complex64) 424 | 425 | #h_tf = tf.constant(h_matrix) 426 | #if isinstance(h_matrix, np.ndarray): 427 | h_tf = tf.convert_to_tensor(h_matrix, dtype=tf.complex64) 428 | 429 | if k == 1: 430 | s_tf = self.s_tf_1 431 | J_tf = self.J_tf_1 432 | else: 433 | s_tf = self.s_tf 434 | J_tf = self.J_tf 435 | 436 | #h_tf = tf.cast(h_tf, tf.complex64) 437 | 438 | H_Jdag = tf.matmul(h_tf, J_tf, adjoint_b=True) 439 | A = tf.matmul(J_tf, H_Jdag) 440 | b = tf.matmul(s_tf, H_Jdag) 441 | B = tf.matmul(b, b, adjoint_a=True) 442 | alpha = tf.matmul(s_tf, tf.matmul(h_tf, s_tf, adjoint_b=True)) 443 | G = A / alpha - B / alpha**2 444 | #if tf.reduce_min(tf.abs(alpha)) < 0.001: 445 | # print('alpha: ', tf.reduce_min(alpha)) 446 | return G 447 | 448 | #@tf.function 449 | def num_FS_volume_form_tf(self, h_matrix, k=-1): 450 | kahler_metric = self.num_kahler_metric_tf(h_matrix, k) 451 | r_tf = self.r_tf 452 | FS_volume_form = tf.matmul(r_tf, tf.matmul(kahler_metric, r_tf, adjoint_b=True)) 453 | FS_volume_form = tf.linalg.det(FS_volume_form) 454 | FS_volume_form = tf.math.real(FS_volume_form) 455 | return FS_volume_form 456 | 457 | def num_kahler_metric(self, h_matrix, point, k=-1): 458 | if k == 1: 459 | # k = 1 will be used in the mass formula during the integration 460 | s = point 461 | # Delete the correspoding row 462 | J = np.delete(np.identity(len(s)), self.norm_coordinate, 0) 463 | else: 464 | s = self.sections(point) 465 | J = self.sections_jacobian(point).T 466 | if isinstance(h_matrix, str): 467 | if h_matrix == 'identity': 468 | h_matrix = np.identity(len(s)) 469 | elif h_matrix == 'FS': 470 | h_matrix = np.array(self.h_FS, dtype=int) 471 | 472 | H_Jdag = np.matmul(h_matrix, np.conj(J).T) 473 | A = np.matmul(J, H_Jdag) 474 | # Get the right half of B then reshape to transpose, 475 | # since b.T is still b if b is a 1d vector 476 | b = np.matmul(s, H_Jdag).reshape(-1, 1) 477 | B = np.matmul(np.conj(b), b.T) 478 | alpha = np.matmul(s, np.matmul(h_matrix, np.conj(s))) 479 | G = A / alpha - B / alpha**2 480 | return G 481 | 482 | def num_FS_volume_form(self, h_matrix, point, k=-1): 483 | kahler_metric = self.num_kahler_metric(h_matrix, point, k) 484 | r = self.restriction(point) 485 | FS_volume_form = np.matmul(r.T, np.matmul(kahler_metric, np.conj(r))) 486 | FS_volume_form = np.matrix(FS_volume_form, dtype=complex) 487 | FS_volume_form = np.linalg.det(FS_volume_form).real 488 | return FS_volume_form 489 | 490 | def diff_conjugate(expr, coordinate): 491 | coord_bar = sp.symbols('coord_bar') 492 | expr_diff = expr.subs(sp.conjugate(coordinate), coord_bar).diff(coord_bar) 493 | expr_diff = expr_diff.subs(coord_bar, sp.conjugate(coordinate)) 494 | return expr_diff 495 | 496 | def diff(expr, coordinate): 497 | coord_bar = sp.symbols('coord_bar') 498 | expr_diff = expr.subs(sp.conjugate(coordinate), coord_bar).diff(coordinate) 499 | expr_diff = expr_diff.subs(coord_bar, sp.conjugate(coordinate)) 500 | return expr_diff 501 | 502 | 503 | class RealHypersurface(Hypersurface): 504 | 505 | def generate_random_projective(self, n_set, n_pt_in_a_set): 506 | z_random= [] 507 | for i in range(n_set): 508 | zv = [] 509 | for j in range(n_pt_in_a_set): 510 | zv.append(np.random.normal(0.0, 1.0, self.n_dim).astype(complex)) 511 | z_random.append(zv) 512 | return z_random 513 | 514 | def solve_points_multiprocessing(self, zpairs, get_coeff): 515 | points = [] 516 | # Multiprocessing. Then append the points to the same list in the main process 517 | with Pool() as pool: 518 | for points_d in pool.starmap(RealHypersurface.solve_poly_real, 519 | zip(zpairs, [get_coeff(zpair[0], zpair[1]) 520 | for zpair in zpairs])): 521 | points.extend(points_d) 522 | return points 523 | 524 | 525 | @staticmethod 526 | def solve_poly_real(zpair, coeff): 527 | # For each zpair there are d solutions, where d is the n_dim 528 | # There will be one real solution and we will keep that only 529 | points_d = [] 530 | try: 531 | c_solved = mpmath.polyroots(coeff) 532 | for pram_c in c_solved: 533 | if np.abs(np.imag(pram_c)) < 1e-8: 534 | points_d.append([complex(pram_c * a + b) 535 | for (a, b) in zip(zpair[0], zpair[1])]) 536 | except: 537 | pass 538 | return points_d 539 | 540 | -------------------------------------------------------------------------------- /src/MLGeometry/lbfgs.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | # 5 | # Distributed under terms of the MIT license. 6 | 7 | """An example of using tfp.optimizer.lbfgs_minimize to optimize a TensorFlow model. 8 | 9 | This code shows a naive way to wrap a tf.keras.Model and optimize it with the L-BFGS 10 | optimizer from TensorFlow Probability. 11 | 12 | Python interpreter version: 3.6.9 13 | TensorFlow version: 2.0.0 14 | TensorFlow Probability version: 0.8.0 15 | NumPy version: 1.17.2 16 | Matplotlib version: 3.1.1 17 | """ 18 | import numpy as np 19 | import tensorflow as tf 20 | import tensorflow_probability as tfp 21 | from matplotlib import pyplot 22 | from . import complex_math 23 | 24 | __all__ = ['function_factory'] 25 | 26 | def function_factory(model, loss, dataset): 27 | """A factory to create a function required by tfp.optimizer.lbfgs_minimize. 28 | 29 | Args: 30 | model [in]: an instance of `tf.keras.Model` or its subclasses. 31 | loss [in]: a function with signature loss_value = loss(pred_y, true_y). 32 | train_x [in]: the input part of training data. 33 | train_y [in]: the output part of training data. 34 | 35 | Returns: 36 | A function that has a signature of: 37 | loss_value, gradients = f(model_parameters). 38 | """ 39 | 40 | # obtain the shapes of all trainable parameters in the model 41 | shapes = tf.shape_n(model.trainable_variables) 42 | n_tensors = len(shapes) 43 | 44 | # we'll use tf.dynamic_stitch and tf.dynamic_partition later, so we need to 45 | # prepare required information first 46 | count = 0 47 | idx = [] # stitch indices 48 | part = [] # partition indices 49 | 50 | for i, shape in enumerate(shapes): 51 | n = np.prod(shape) 52 | idx.append(tf.reshape(tf.range(count, count+n, dtype=tf.int32), shape)) 53 | part.extend([i]*n) 54 | count += n 55 | 56 | part = tf.constant(part) 57 | 58 | @tf.function 59 | @tf.autograph.experimental.do_not_convert 60 | def assign_new_model_parameters(params_1d): 61 | """A function updating the model's parameters with a 1D tf.Tensor. 62 | 63 | Args: 64 | params_1d [in]: a 1D tf.Tensor representing the model's trainable parameters. 65 | """ 66 | 67 | params = tf.dynamic_partition(params_1d, part, n_tensors) 68 | for i, (shape, param) in enumerate(zip(shapes, params)): 69 | model.trainable_variables[i].assign(tf.reshape(param, shape)) 70 | #tf.print(model.trainable_variables[i]) 71 | 72 | @tf.function 73 | def volume_form(x, Omega_Omegabar, mass, restriction): 74 | kahler_metric = complex_math.complex_hessian(tf.math.real(model(x)), x) 75 | volume_form = tf.math.real(tf.linalg.det(tf.matmul(restriction, tf.matmul(kahler_metric, restriction, adjoint_b=True)))) 76 | weights = mass / tf.reduce_sum(mass) 77 | factor = tf.reduce_sum(weights * volume_form / Omega_Omegabar) 78 | #factor = tf.constant(35.1774, dtype=tf.complex64) 79 | return volume_form / factor 80 | 81 | 82 | # now create a function that will be returned by this factory 83 | def f(params_1d): 84 | """A function that can be used by tfp.optimizer.lbfgs_minimize. 85 | 86 | This function is created by function_factory. 87 | 88 | Args: 89 | params_1d [in]: a 1D tf.Tensor. 90 | 91 | Returns: 92 | A scalar loss and the gradients w.r.t. the `params_1d`. 93 | """ 94 | 95 | # use GradientTape so that we can calculate the gradient of loss w.r.t. parameters 96 | for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset): 97 | with tf.GradientTape() as tape: 98 | # update the parameters in the model 99 | assign_new_model_parameters(params_1d) 100 | # calculate the loss 101 | det_omega = volume_form(points, Omega_Omegabar, mass, restriction) 102 | loss_value = loss(Omega_Omegabar, det_omega, mass) 103 | 104 | # calculate gradients and convert to 1D tf.Tensor 105 | grads = tape.gradient(loss_value, model.trainable_variables) 106 | grads = tf.dynamic_stitch(idx, grads) 107 | 108 | # reweight the loss and grads 109 | mass_sum = tf.reduce_sum(mass) 110 | try: 111 | total_loss += loss_value * mass_sum 112 | total_grads += grads * mass_sum 113 | total_mass += mass_sum 114 | except NameError: 115 | total_loss = loss_value * mass_sum 116 | total_grads = grads * mass_sum 117 | total_mass = mass_sum 118 | 119 | total_loss = total_loss / total_mass 120 | total_grads = total_grads / total_mass 121 | 122 | # print out iteration & loss 123 | f.iter.assign_add(1) 124 | tf.print("Iter:", f.iter, "loss:", total_loss) 125 | 126 | # store loss value so we can retrieve later 127 | tf.py_function(f.history.append, inp=[total_loss], Tout=[]) 128 | 129 | return total_loss, total_grads 130 | 131 | # store these information as members so we can use them outside the scope 132 | f.iter = tf.Variable(0) 133 | f.idx = idx 134 | f.part = part 135 | f.shapes = shapes 136 | f.assign_new_model_parameters = assign_new_model_parameters 137 | f.history = [] 138 | 139 | return f 140 | -------------------------------------------------------------------------------- /src/MLGeometry/loss.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import keras 3 | 4 | __all__ = ['weighted_MAPE','weighted_MSE','max_error','MAPE_plus_max_error'] 5 | 6 | @keras.saving.register_keras_serializable(package="MLGeometry") 7 | def weighted_MAPE(y_true, y_pred, mass): 8 | weights = mass / tf.reduce_sum(mass) 9 | return tf.reduce_sum(tf.abs(y_true - y_pred) / y_true * weights) 10 | 11 | 12 | @keras.saving.register_keras_serializable(package="MLGeometry") 13 | def weighted_MSE(y_true, y_pred, mass): 14 | weights = mass / tf.reduce_sum(mass) 15 | return tf.reduce_sum(tf.square(y_pred / y_true - 1) * weights) 16 | 17 | 18 | @keras.saving.register_keras_serializable(package="MLGeometry") 19 | def max_error(y_true, y_pred, mass): 20 | return tf.math.reduce_max(tf.abs(y_true - y_pred) / y_true) 21 | 22 | 23 | @keras.saving.register_keras_serializable(package="MLGeometry") 24 | def MAPE_plus_max_error(y_true, y_pred, mass): 25 | return 1*max_error(y_true, y_pred, mass) + weighted_MAPE(y_true, y_pred, mass) 26 | 27 | -------------------------------------------------------------------------------- /src/MLGeometry/tf_dataset.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | import numpy as np 3 | 4 | __all__ = ['generate_dataset', 'dataset_on_patch'] 5 | 6 | def generate_dataset(patch): 7 | dataset = None 8 | if patch.patches == []: 9 | dataset = dataset_on_patch(patch) 10 | else: 11 | for subpatch in patch.patches: 12 | new_dataset = generate_dataset(subpatch) 13 | if dataset is None: 14 | dataset = new_dataset 15 | else: 16 | dataset = dataset.concatenate(new_dataset) 17 | return dataset 18 | 19 | def dataset_on_patch(patch): 20 | 21 | # To calculate the numerical tensors, one needs to invoke function set_k() first 22 | # to lambdify the sympy expression to generate the python functions used for different k. 23 | # However the full set of set_k() is too slow for large k. Here the minimum 24 | # required functions are invoked so that one does not need to invoke set_k(). 25 | patch.s_tf_1, patch.J_tf_1 = patch.num_s_J_tf(k=1) 26 | patch.omega_omegabar = patch.get_omega_omegabar(lambdify=True) 27 | patch.restriction = patch.get_restriction(lambdify=True) 28 | patch.r_tf = patch.num_restriction_tf() 29 | 30 | x = tf.convert_to_tensor(np.array(patch.points, dtype=np.complex64)) 31 | y = tf.cast(patch.num_Omega_Omegabar_tf(), dtype=tf.float32) 32 | 33 | mass = y / tf.cast(patch.num_FS_volume_form_tf('identity', k=1), dtype=tf.float32) 34 | 35 | # The Kahler metric calculated by complex_hessian includes the derivative of 36 | # the norm_coordinate. Here the restriction is linear transformed so that 37 | # the corresponding column and row will be ignored in the hessian. 38 | trans_mat = np.delete(np.identity(patch.n_dim), patch.norm_coordinate, axis=0) 39 | trans_tensor = tf.convert_to_tensor(np.array(trans_mat, dtype=np.complex64)) 40 | restriction = tf.matmul(patch.r_tf, trans_tensor) 41 | 42 | dataset = tf.data.Dataset.from_tensor_slices((x, y, mass, restriction)) 43 | 44 | return dataset 45 | 46 | -------------------------------------------------------------------------------- /training/README.md: -------------------------------------------------------------------------------- 1 | These are the training scripts. Put them in the root folder then run the .sh file 2 | -------------------------------------------------------------------------------- /training/bihomoNN_train.py: -------------------------------------------------------------------------------- 1 | import os 2 | #os.environ['CUDA_VISIBLE_DEVICES'] = '0' 3 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' 4 | import sys 5 | sys.path.append("..") 6 | 7 | import tensorflow as tf 8 | import tensorflow_probability as tfp 9 | import numpy as np 10 | import sympy as sp 11 | import time 12 | import math 13 | import argparse 14 | 15 | import MLGeometry as mlg 16 | from models import * 17 | 18 | z0, z1, z2, z3, z4 = sp.symbols('z0, z1, z2, z3, z4') 19 | Z = [z0,z1,z2,z3,z4] 20 | 21 | parser = argparse.ArgumentParser() 22 | # Data generation 23 | parser.add_argument('--seed', type=int) 24 | parser.add_argument('--n_pairs', type=int) 25 | parser.add_argument('--batch_size', type=int) 26 | parser.add_argument('--function') 27 | parser.add_argument('--psi', type=float) 28 | parser.add_argument('--phi', type=float) 29 | parser.add_argument('--alpha', type=float) 30 | 31 | # Network 32 | parser.add_argument('--OuterProductNN_k', type=int) 33 | parser.add_argument('--layers') 34 | parser.add_argument('--k2_as_first_layer', action='store_true') 35 | parser.add_argument('--k4_as_first_layer', action='store_true') 36 | parser.add_argument('--load_model') 37 | parser.add_argument('--save_dir') 38 | parser.add_argument('--save_name') 39 | 40 | # Training 41 | parser.add_argument('--max_epochs', type=int) 42 | parser.add_argument('--loss_func') 43 | parser.add_argument('--clip_threshold', type=float) 44 | parser.add_argument('--optimizer', default='Adam') 45 | parser.add_argument('--learning_rate', type=float, default=0.001) 46 | parser.add_argument('--decay_rate', type=float, default=1.0) 47 | parser.add_argument('--num_correction_pairs', type=int, default=10) 48 | 49 | args = parser.parse_args() 50 | print("Processing model: " + args.save_name) 51 | # Data generation 52 | seed = args.seed 53 | n_pairs = args.n_pairs 54 | batch_size = args.batch_size 55 | psi = args.psi 56 | 57 | f = z0**5 + z1**5 + z2**5 + z3**5 + z4**5 + psi*z0*z1*z2*z3*z4 58 | if args.function == 'f1': 59 | phi = args.phi 60 | f = f + phi*(z3*z4**4 + z3**2*z4**3 + z3**3*z4**2 + z3**4*z4) 61 | elif args.function == 'f2': 62 | alpha = args.alpha 63 | f = f + alpha*(z2*z0**4 + z0*z4*z1**3 + z0*z2*z3*z4**2 + z3**2*z1**3 + z4*z1**2*z2**2 + z0*z1*z2*z3**2 + 64 | z2*z4*z3**3 + z0*z1**4 + z0*z4**2*z2**2 + z4**3*z1**2 + z0*z2*z3**3 + z3*z4*z0**3 + z1**3*z4**2 + 65 | z0*z2*z4*z1**2 + z1**2*z3**3 + z1*z4**4 + z1*z2*z0**3 + z2**2*z4**3 + z4*z2**4 + z1*z3**4) 66 | 67 | np.random.seed(seed) 68 | tf.random.set_seed(seed) 69 | HS = mlg.hypersurface.Hypersurface(Z, f, n_pairs) 70 | HS_test = mlg.hypersurface.Hypersurface(Z, f, n_pairs) 71 | 72 | train_set = mlg.tf_dataset.generate_dataset(HS) 73 | test_set = mlg.tf_dataset.generate_dataset(HS_test) 74 | 75 | #if batch_size is None or args.optimizer.lower() == 'lbfgs': 76 | if batch_size is None: 77 | batch_size = HS.n_points 78 | 79 | train_set = train_set.shuffle(HS.n_points).batch(batch_size) 80 | test_set = test_set.shuffle(HS_test.n_points).batch(batch_size) 81 | 82 | # Network 83 | if args.OuterProductNN_k is not None: 84 | k = args.OuterProductNN_k 85 | else: 86 | layers = args.layers 87 | n_units = layers.split('_') 88 | for i in range(0, len(n_units)): 89 | n_units[i] = int(n_units[i]) 90 | n_hidden = len(n_units) - 1 91 | if args.k2_as_first_layer is True: 92 | k = 2**(n_hidden+1) 93 | else: 94 | k = 2**n_hidden 95 | 96 | model_list_OuterProductNN = [OuterProductNN_k2, OuterProductNN_k3, OuterProductNN_k4] 97 | model_list_k2_as_first_layer = [k2_twolayers, k2_threelayers] 98 | model_list_k4_as_first_layer = [k4_onelayer, k4_twolayers] 99 | model_list = [zerolayer, onelayer, twolayers, threelayers, fourlayers, fivelayers] 100 | 101 | load_path = args.load_model 102 | if load_path is not None: 103 | model = keras.models.load_model(load_path, compile=False) 104 | elif args.OuterProductNN_k is not None: 105 | try: 106 | model = model_list_OuterProductNN[k-2]() 107 | except IndexError: 108 | print("Error: Only k = 2,3,4 are supported now") 109 | elif args.k2_as_first_layer: 110 | try: 111 | model = model_list_k2_as_first_layer[n_hidden-2](n_units) 112 | except IndexError: 113 | print("Error: Only two and three layers are supported") 114 | elif args.k4_as_first_layer: 115 | try: 116 | model = model_list_k4_as_first_layer[n_hidden-1](n_units) 117 | except IndexError: 118 | print("Error: Only one and two layers is supported") 119 | else: 120 | try: 121 | model = model_list[n_hidden](n_units) 122 | except IndexError: 123 | print("Error: Only k <= 32 is supported") 124 | 125 | 126 | max_epochs = args.max_epochs 127 | func_dict = {"weighted_MAPE": mlg.loss.weighted_MAPE, "weighted_MSE": mlg.loss.weighted_MSE, "max_error":mlg.loss.max_error, 128 | "MAPE_plus_max_error": mlg.loss.MAPE_plus_max_error} 129 | loss_func = func_dict[args.loss_func] 130 | #early_stopping = False 131 | clip_threshold = args.clip_threshold 132 | save_dir = args.save_dir 133 | if not os.path.exists(save_dir): 134 | os.makedirs(save_dir) 135 | save_name = args.save_name 136 | 137 | @tf.function 138 | def volume_form(x, Omega_Omegabar, mass, restriction): 139 | kahler_metric = mlg.complex_math.complex_hessian(tf.math.real(model(x)), x) 140 | volume_form = tf.math.real(tf.linalg.det(tf.matmul(restriction, tf.matmul(kahler_metric, restriction, adjoint_b=True)))) 141 | weights = mass / tf.reduce_sum(mass) 142 | factor = tf.reduce_sum(weights * volume_form / Omega_Omegabar) 143 | #factor = tf.constant(35.1774, dtype=tf.complex64) 144 | return volume_form / factor 145 | 146 | def cal_total_loss(dataset, loss_function): 147 | 148 | total_loss = tf.constant(0, dtype=tf.float32) 149 | total_mass= tf.constant(0, dtype=tf.float32) 150 | 151 | for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset): 152 | det_omega = volume_form(points, Omega_Omegabar, mass, restriction) 153 | mass_sum = tf.reduce_sum(mass) 154 | total_loss += loss_function(Omega_Omegabar, det_omega, mass) * mass_sum 155 | total_mass += mass_sum 156 | total_loss = total_loss / total_mass 157 | 158 | return total_loss.numpy() 159 | 160 | def cal_max_error(dataset): 161 | ''' 162 | find max|eta - 1| over the whole dataset: calculate the error on each batch then compare. 163 | ''' 164 | max_error_tmp = 0 165 | for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset): 166 | det_omega = volume_form(points, Omega_Omegabar, mass, restriction) 167 | error = mlg.loss.max_error(Omega_Omegabar, det_omega, mass).numpy() 168 | if error > max_error_tmp: 169 | max_error_tmp = error 170 | 171 | return max_error_tmp 172 | 173 | # Training 174 | start_time = time.time() 175 | if args.optimizer.lower() == 'lbfgs': 176 | # iter+1 everytime f is evoked, which will also be invoked when calculationg the hessian, etc 177 | # So the true max_epochs will be 3 times user's input 178 | max_epochs = int(max_epochs/3) 179 | train_func = mlg.lbfgs.function_factory(model, loss_func, train_set) 180 | 181 | init_params = tf.dynamic_stitch(train_func.idx, model.trainable_variables) 182 | results = tfp.optimizer.lbfgs_minimize(value_and_gradients_function=train_func, 183 | initial_position=init_params, 184 | max_iterations=max_epochs, 185 | num_correction_pairs=args.num_correction_pairs) 186 | train_func.assign_new_model_parameters(results.position) 187 | 188 | else: 189 | if args.optimizer.lower() == 'sgd': 190 | optimizer = keras.optimizers.SGD(args.learning_rate) 191 | else: 192 | lr_schedule = keras.optimizers.schedules.ExponentialDecay( 193 | initial_learning_rate=args.learning_rate, 194 | decay_steps = HS.n_points/batch_size, 195 | decay_rate = args.decay_rate) 196 | optimizer = keras.optimizers.Adam(learning_rate=lr_schedule) 197 | #optimizer = keras.optimizers.Adam(learning_rate=args.learning_rate) 198 | 199 | train_log_dir = save_dir + '/logs/' + save_name + '/train' 200 | test_log_dir = save_dir + '/logs/' + save_name + '/test' 201 | train_summary_writer = tf.summary.create_file_writer(train_log_dir) 202 | test_summary_writer = tf.summary.create_file_writer(test_log_dir) 203 | 204 | stop = False 205 | loss_old = 100000 206 | epoch = 0 207 | 208 | while epoch < max_epochs and stop is False: 209 | epoch = epoch + 1 210 | for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set): 211 | with tf.GradientTape() as tape: 212 | 213 | det_omega = volume_form(points, Omega_Omegabar, mass, restriction) 214 | loss = loss_func(Omega_Omegabar, det_omega, mass) 215 | grads = tape.gradient(loss, model.trainable_weights) 216 | if clip_threshold is not None: 217 | grads = [tf.clip_by_value(grad, -clip_threshold, clip_threshold) for grad in grads] 218 | optimizer.apply_gradients(zip(grads, model.trainable_weights)) 219 | #tf.print(model.tranable_weights) 220 | #if step % 500 == 0: 221 | # print("step %d: loss = %.4f" % (step, loss)) 222 | if epoch % 10 == 0: 223 | sigma_max_train = cal_max_error(train_set) 224 | sigma_max_test = cal_max_error(test_set) 225 | 226 | E_train = cal_total_loss(train_set, mlg.loss.weighted_MSE) 227 | E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE) 228 | 229 | sigma_train = cal_total_loss(train_set, mlg.loss.weighted_MAPE) 230 | sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE) 231 | 232 | def delta_sigma_square_train(y_true, y_pred, mass): 233 | weights = mass / tf.reduce_sum(mass) 234 | return tf.reduce_sum((tf.abs(y_true - y_pred) / y_true - sigma_train)**2 * weights) 235 | 236 | def delta_sigma_square_test(y_true, y_pred, mass): 237 | weights = mass / tf.reduce_sum(mass) 238 | return tf.reduce_sum((tf.abs(y_true - y_pred) / y_true - sigma_test)**2 * weights) 239 | 240 | delta_sigma_train = math.sqrt(cal_total_loss(train_set, delta_sigma_square_train) / HS.n_points) 241 | delta_sigma_test = math.sqrt(cal_total_loss(test_set, delta_sigma_square_test) / HS.n_points) 242 | 243 | print("train_loss:", loss.numpy()) 244 | print("test_loss:", cal_total_loss(test_set, loss_func)) 245 | 246 | with train_summary_writer.as_default(): 247 | tf.summary.scalar('max_error', sigma_max_train, step=epoch) 248 | tf.summary.scalar('delta_sigma', delta_sigma_train, step=epoch) 249 | tf.summary.scalar('E', E_train, step=epoch) 250 | tf.summary.scalar('sigma', sigma_train , step=epoch) 251 | 252 | with test_summary_writer.as_default(): 253 | tf.summary.scalar('max_error', sigma_max_test, step=epoch) 254 | tf.summary.scalar('delta_sigma', delta_sigma_test, step=epoch) 255 | tf.summary.scalar('E', E_test, step=epoch) 256 | tf.summary.scalar('sigma', sigma_test, step=epoch) # Early stopping 257 | 258 | # if early_stopping is True and epoch > 800: 259 | # if epoch % 5 == 0: 260 | # if train_loss > loss_old: 261 | # stop = True 262 | # loss_old = train_loss 263 | 264 | train_time = time.time() - start_time 265 | 266 | model.save(save_dir + '/' + save_name) 267 | 268 | sigma_train = cal_total_loss(train_set, mlg.loss.weighted_MAPE) 269 | sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE) 270 | E_train = cal_total_loss(train_set, mlg.loss.weighted_MSE) 271 | E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE) 272 | sigma_max_train = cal_max_error(train_set) 273 | sigma_max_test = cal_max_error(test_set) 274 | 275 | ####################################################################### 276 | # Calculate delta_sigma 277 | 278 | def delta_sigma_square_train(y_true, y_pred, mass): 279 | weights = mass / tf.reduce_sum(mass) 280 | return tf.reduce_sum((tf.abs(y_true - y_pred) / y_true - sigma_train)**2 * weights) 281 | 282 | def delta_sigma_square_test(y_true, y_pred, mass): 283 | weights = mass / tf.reduce_sum(mass) 284 | return tf.reduce_sum((tf.abs(y_true - y_pred) / y_true - sigma_test)**2 * weights) 285 | 286 | def delta_E_square_train(y_true, y_pred, mass): 287 | weights = mass / tf.reduce_sum(mass) 288 | return tf.reduce_sum(((y_pred / y_true - 1)**2 - E_train)**2 * weights) 289 | 290 | def delta_E_square_test(y_true, y_pred, mass): 291 | weights = mass / tf.reduce_sum(mass) 292 | return tf.reduce_sum(((y_pred / y_true - 1)**2 - E_test)**2 * weights) 293 | 294 | delta_sigma_train = math.sqrt(cal_total_loss(train_set, delta_sigma_square_train) / HS.n_points) 295 | delta_sigma_test = math.sqrt(cal_total_loss(test_set, delta_sigma_square_test) / HS.n_points) 296 | delta_E_train = math.sqrt(cal_total_loss(train_set, delta_E_square_train) / HS.n_points) 297 | delta_E_test = math.sqrt(cal_total_loss(test_set, delta_E_square_test) / HS.n_points) 298 | 299 | #print(delta_sigma_train) 300 | #print(delta_sigma_test) 301 | 302 | ##################################################################### 303 | # Write to file 304 | 305 | with open(save_dir + save_name + ".txt", "w") as f: 306 | f.write('[Results] \n') 307 | f.write('model_name = {} \n'.format(save_name)) 308 | f.write('seed = {} \n'.format(seed)) 309 | f.write('n_pairs = {} \n'.format(n_pairs)) 310 | f.write('n_points = {} \n'.format(HS.n_points)) 311 | f.write('batch_size = {} \n'.format(batch_size)) 312 | f.write('function = {} \n'.format(args.function)) 313 | f.write('psi = {} \n'.format(psi)) 314 | if args.function == 'f1': 315 | f.write('phi = {} \n'.format(phi)) 316 | elif args.function == 'f2': 317 | f.write('alpha = {} \n'.format(alpha)) 318 | f.write('k = {} \n'.format(k)) 319 | f.write('n_parameters = {} \n'.format(model.count_params())) 320 | f.write('loss function = {} \n'.format(loss_func.__name__)) 321 | if clip_threshold is not None: 322 | f.write('clip_threshold = {} \n'.format(clip_threshold)) 323 | f.write('\n') 324 | f.write('n_epochs = {} \n'.format(max_epochs)) 325 | f.write('train_time = {:.6g} \n'.format(train_time)) 326 | f.write('sigma_train = {:.6g} \n'.format(sigma_train)) 327 | f.write('sigma_test = {:.6g} \n'.format(sigma_test)) 328 | f.write('delta_sigma_train = {:.6g} \n'.format(delta_sigma_train)) 329 | f.write('delta_sigma_test = {:.6g} \n'.format(delta_sigma_test)) 330 | f.write('E_train = {:.6g} \n'.format(E_train)) 331 | f.write('E_test = {:.6g} \n'.format(E_test)) 332 | f.write('delta_E_train = {:.6g} \n'.format(delta_E_train)) 333 | f.write('delta_E_test = {:.6g} \n'.format(delta_E_test)) 334 | f.write('sigma_max_train = {:.6g} \n'.format(sigma_max_train)) 335 | f.write('sigma_max_test = {:.6g} \n'.format(sigma_max_test)) 336 | 337 | with open(save_dir + "summary.txt", "a") as f: 338 | if args.function == 'f0': 339 | f.write('{} {} {} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g}\n'.format(save_name, args.function, psi, train_time, sigma_train, sigma_test, E_train, E_test, sigma_max_train, sigma_max_test)) 340 | elif args.function == 'f1': 341 | f.write('{} {} {} {} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g}\n'.format(save_name, args.function, psi, phi, train_time, sigma_train, sigma_test, E_train, E_test, sigma_max_train, sigma_max_test)) 342 | elif args.function == 'f2': 343 | f.write('{} {} {} {} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g} {:.6g}\n'.format(save_name, args.function, psi, alpha, train_time, sigma_train, sigma_test, E_train, E_test, sigma_max_train, sigma_max_test)) 344 | -------------------------------------------------------------------------------- /training/bihomoNN_train.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for psi in 0.5 ; do 3 | for loss_func in "weighted_MAPE" ; do 4 | for layers in "300_300_300_1"; do 5 | python bihomoNN_train.py --seed 1234 \ 6 | --n_pairs 100000\ 7 | --batch_size 5000\ 8 | --function "f0" \ 9 | --psi $psi \ 10 | --layers $layers \ 11 | --load_model "f0_psi${psi}/${layers}" \ 12 | --save_dir "experiments.yidi/train_curve/f0_psi${psi}/" \ 13 | --save_name "${layers}" \ 14 | --optimizer 'lbfgs'\ 15 | --learning_rate 0.001 \ 16 | --decay_rate 1 \ 17 | --max_epochs 1000\ 18 | --loss_func ${loss_func} 19 | done 20 | done 21 | done 22 | -------------------------------------------------------------------------------- /training/models.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from MLGeometry import bihomoNN as bnn 3 | 4 | __all__ = ['zerolayer', 'onelayer', 'twolayers', 'threelayers', 'fourlayers', 5 | 'fivelayers','OuterProductNN_k2','OuterProductNN_k3','OuterProductNN_k4', 6 | 'k2_twolayers', 'k2_threelayers','k4_onelayer','k4_twolayers'] 7 | 8 | class zerolayer(keras.Model): 9 | 10 | def __init__(self, n_units): 11 | super(zerolayer, self).__init__() 12 | self.bihomogeneous = bnn.Bihomogeneous() 13 | self.layer1 = bnn.WidthOneDense(25, 1) 14 | 15 | def call(self, inputs): 16 | x = self.bihomogeneous(inputs) 17 | x = self.layer1(x) 18 | x = tf.math.log(x) 19 | return x 20 | 21 | class onelayer(keras.Model): 22 | 23 | def __init__(self, n_units): 24 | super(onelayer, self).__init__() 25 | self.bihomogeneous = bnn.Bihomogeneous() 26 | self.layer1 = bnn.SquareDense(25, n_units[0], activation=tf.square) 27 | self.layer2 = bnn.SquareDense(n_units[0], 1) 28 | 29 | def call(self, inputs): 30 | x = self.bihomogeneous(inputs) 31 | x = self.layer1(x) 32 | x = self.layer2(x) 33 | x = tf.math.log(x) 34 | return x 35 | 36 | 37 | class twolayers(keras.Model): 38 | 39 | def __init__(self, n_units): 40 | super(twolayers, self).__init__() 41 | self.bihomogeneous = bnn.Bihomogeneous() 42 | self.layer1 = bnn.SquareDense(25, n_units[0], activation=tf.square) 43 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 44 | self.layer3 = bnn.SquareDense(n_units[1], 1) 45 | 46 | def call(self, inputs): 47 | x = self.bihomogeneous(inputs) 48 | x = self.layer1(x) 49 | x = self.layer2(x) 50 | x = self.layer3(x) 51 | x = tf.math.log(x) 52 | return x 53 | 54 | 55 | class threelayers(keras.Model): 56 | 57 | def __init__(self, n_units): 58 | super(threelayers, self).__init__() 59 | self.bihomogeneous = bnn.Bihomogeneous() 60 | self.layer1 = bnn.SquareDense(25, n_units[0], activation=tf.square) 61 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 62 | self.layer3 = bnn.SquareDense(n_units[1], n_units[2], activation=tf.square) 63 | self.layer4 = bnn.SquareDense(n_units[2], 1) 64 | 65 | def call(self, inputs): 66 | x = self.bihomogeneous(inputs) 67 | x = self.layer1(x) 68 | x = self.layer2(x) 69 | x = self.layer3(x) 70 | x = self.layer4(x) 71 | x = tf.math.log(x) 72 | return x 73 | 74 | 75 | class fourlayers(keras.Model): 76 | 77 | def __init__(self, n_units): 78 | super(fourlayers, self).__init__() 79 | self.bihomogeneous = bnn.Bihomogeneous() 80 | self.layer1 = bnn.SquareDense(25, n_units[0], activation=tf.square) 81 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 82 | self.layer3 = bnn.SquareDense(n_units[1], n_units[2], activation=tf.square) 83 | self.layer4 = bnn.SquareDense(n_units[2], n_units[3], activation=tf.square) 84 | self.layer5 = bnn.SquareDense(n_units[3], 1) 85 | 86 | def call(self, inputs): 87 | x = self.bihomogeneous(inputs) 88 | x = self.layer1(x) 89 | x = self.layer2(x) 90 | x = self.layer3(x) 91 | x = self.layer4(x) 92 | x = self.layer5(x) 93 | x = tf.math.log(x) 94 | return x 95 | 96 | 97 | class fivelayers(keras.Model): 98 | 99 | def __init__(self, n_units): 100 | super(fivelayers, self).__init__() 101 | self.bihomogeneous = bnn.Bihomogeneous() 102 | self.layer1 = bnn.SquareDense(25, n_units[0], activation=tf.square) 103 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 104 | self.layer3 = bnn.SquareDense(n_units[1], n_units[2], activation=tf.square) 105 | self.layer4 = bnn.SquareDense(n_units[2], n_units[3], activation=tf.square) 106 | self.layer5 = bnn.SquareDense(n_units[3], n_units[4], activation=tf.square) 107 | self.layer6 = bnn.SquareDense(n_units[4], 1) 108 | 109 | def call(self, inputs): 110 | x = self.bihomogeneous(inputs) 111 | x = self.layer1(x) 112 | x = self.layer2(x) 113 | x = self.layer3(x) 114 | x = self.layer4(x) 115 | x = self.layer5(x) 116 | x = self.layer6(x) 117 | x = tf.math.log(x) 118 | return x 119 | 120 | class OuterProductNN_k2(keras.Model): 121 | 122 | def __init__(self): 123 | super(OuterProductNN_k2, self).__init__() 124 | self.bihomogeneous_k2 = bnn.Bihomogeneous_k2() 125 | self.layer1 = bnn.WidthOneDense(15**2, 1) 126 | 127 | def call(self, inputs): 128 | x = self.bihomogeneous_k2(inputs) 129 | x = self.layer1(x) 130 | x = tf.math.log(x) 131 | return x 132 | 133 | 134 | class OuterProductNN_k3(keras.Model): 135 | 136 | def __init__(self): 137 | super(OuterProductNN_k3, self).__init__() 138 | self.bihomogeneous_k3 = bnn.Bihomogeneous_k3() 139 | self.layer1 = bnn.WidthOneDense(35**2, 1) 140 | 141 | def call(self, inputs): 142 | x = self.bihomogeneous_k3(inputs) 143 | x = self.layer1(x) 144 | x = tf.math.log(x) 145 | return x 146 | 147 | class OuterProductNN_k4(keras.Model): 148 | 149 | def __init__(self): 150 | super(OuterProductNN_k4, self).__init__() 151 | self.bihomogeneous_k4 = bnn.Bihomogeneous_k4() 152 | self.layer1 = bnn.WidthOneDense(70**2, 1) 153 | 154 | def call(self, inputs): 155 | with tf.device('/cpu:0'): 156 | x = self.bihomogeneous_k4(inputs) 157 | with tf.device('/gpu:0'): 158 | x = self.layer1(x) 159 | x = tf.math.log(x) 160 | return x 161 | 162 | class k2_twolayers(keras.Model): 163 | 164 | def __init__(self, n_units): 165 | super(k2_twolayers, self).__init__() 166 | self.bihomogeneous_k2 = bnn.Bihomogeneous_k2() 167 | self.layer1 = bnn.SquareDense(15**2, n_units[0], activation=tf.square) 168 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 169 | self.layer3 = bnn.SquareDense(n_units[1], 1) 170 | 171 | def call(self, inputs): 172 | x = self.bihomogeneous_k2(inputs) 173 | x = self.layer1(x) 174 | x = self.layer2(x) 175 | x = self.layer3(x) 176 | x = tf.math.log(x) 177 | return x 178 | 179 | 180 | class k2_threelayers(keras.Model): 181 | 182 | def __init__(self, n_units): 183 | super(k2_threelayers, self).__init__() 184 | self.bihomogeneous_k2 = bnn.Bihomogeneous_k2() 185 | self.layer1 = bnn.SquareDense(15**2, n_units[0], activation=tf.square) 186 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 187 | self.layer3 = bnn.SquareDense(n_units[1], n_units[2], activation=tf.square) 188 | self.layer4 = bnn.SquareDense(n_units[2], 1) 189 | 190 | def call(self, inputs): 191 | x = self.bihomogeneous_k2(inputs) 192 | x = self.layer1(x) 193 | x = self.layer2(x) 194 | x = self.layer3(x) 195 | x = self.layer4(x) 196 | x = tf.math.log(x) 197 | return x 198 | 199 | class k4_onelayer(keras.Model): 200 | 201 | def __init__(self, n_units): 202 | super(k4_onelayer, self).__init__() 203 | self.bihomogeneous_k4 = bnn.Bihomogeneous_k4() 204 | self.layer1 = bnn.SquareDense(70**2, n_units[0], activation=tf.square) 205 | self.layer2 = bnn.SquareDense(n_units[0], 1) 206 | 207 | def call(self, inputs): 208 | x = self.bihomogeneous_k4(inputs) 209 | x = self.layer1(x) 210 | x = self.layer2(x) 211 | x = tf.math.log(x) 212 | return x 213 | 214 | class k4_twolayers(keras.Model): 215 | 216 | def __init__(self, n_units): 217 | super(k4_twolayers, self).__init__() 218 | self.bihomogeneous_k4 = bnn.Bihomogeneous_k4() 219 | self.layer1 = bnn.SquareDense(70**2, n_units[0], activation=tf.square) 220 | self.layer2 = bnn.SquareDense(n_units[0], n_units[1], activation=tf.square) 221 | self.layer3 = bnn.SquareDense(n_units[1], 1) 222 | 223 | def call(self, inputs): 224 | x = self.bihomogeneous_k4(inputs) 225 | x = self.layer1(x) 226 | x = self.layer2(x) 227 | x = self.layer3(x) 228 | x = tf.math.log(x) 229 | return x 230 | 231 | --------------------------------------------------------------------------------