├── .gitignore ├── CITATION.bib ├── LICENSE ├── LICENSE-model ├── README.md ├── example_data ├── oasis.py └── wbc.py ├── requirements.txt ├── setup.py └── universeg ├── __init__.py ├── model.py ├── nn ├── __init__.py ├── cross_conv.py ├── init.py └── vmap.py └── validation.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @article{butoi2023universeg, 2 | title = {UniverSeg: Universal Medical Image Segmentation}, 3 | author = {Victor Ion Butoi* and Jose Javier Gonzalez Ortiz* and Tianyu Ma and 4 | Mert R. Sabuncu and John Guttag and Adrian V. Dalca}, 5 | journal = {arXiv:2304.02643}, 6 | year = {2023}, 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-model: -------------------------------------------------------------------------------- 1 | OpenRAIL++-M License Template 2 | 3 | Copyright (c) 2023 UniverSeg authors 4 | 5 | UniverSeg Open RAIL++-M 6 | dated August 22, 2022 7 | 8 | Section I: PREAMBLE 9 | 10 | NOW THEREFORE, You and Licensor agree as follows: 11 | 12 | 1. Definitions 13 | 14 | - "License" means the terms and conditions for use, reproduction, and Distribution as defined in this document. 15 | - "Data" means a collection of information and/or content extracted from the dataset used with the Model, including to train, pretrain, or otherwise evaluate the Model. The Data is not licensed under this License. 16 | - "Output" means the results of operating a Model as embodied in informational content resulting therefrom. 17 | - "Model" means any accompanying machine-learning based assemblies (including checkpoints), consisting of learnt weights, parameters (including optimizer states), corresponding to the model architecture as embodied in the Complementary Material, that have been trained or tuned, in whole or in part on the Data, using the Complementary Material. 18 | - "Derivatives of the Model" means all modifications to the Model, works based on the Model, or any other model which is created or initialized by transfer of patterns of the weights, parameters, activations or output of the Model, to the other model, in order to cause the other model to perform similarly to the Model, including - but not limited to - distillation methods entailing the use of intermediate data representations or methods based on the generation of synthetic data by the Model for training the other model. 19 | - "Complementary Material" means the accompanying source code and scripts used to define, run, load, benchmark or evaluate the Model, and used to prepare data for training or evaluation, if any. This includes any accompanying documentation, tutorials, examples, etc, if any. 20 | - "Distribution" means any transmission, reproduction, publication or other sharing of the Model or Derivatives of the Model to a third party, including providing the Model as a hosted service made available by electronic or other remote means - e.g. API-based or web access. 21 | - "Licensor" means the copyright owner or entity authorized by the copyright owner that is granting the License, including the persons or entities that may have rights in the Model and/or distributing the Model. 22 | - "You" (or "Your") means an individual or Legal Entity exercising permissions granted by this License and/or making use of the Model for whichever purpose and in any field of use, including usage of the Model in an end-use application - e.g. chatbot, translator, image generator. 23 | - "Third Parties" means individuals or legal entities that are not under common control with Licensor or You. 24 | - "Contribution" means any work of authorship, including the original version of the Model and any modifications or additions to that Model or Derivatives of the Model thereof, that is intentionally submitted to Licensor for inclusion in the Model by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Model, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 25 | - "Contributor" means Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Model. 26 | 27 | Section II: INTELLECTUAL PROPERTY RIGHTS 28 | 29 | Both copyright and patent grants apply to the Model, Derivatives of the Model and Complementary Material. The Model and Derivatives of the Model are subject to additional terms as described in Section III. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare, publicly display, publicly perform, sublicense, and distribute the Complementary Material, the Model, and Derivatives of the Model. 32 | 3. Grant of Patent License. Subject to the terms and conditions of this License and where and as applicable, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this paragraph) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Model and the Complementary Material, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Model to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Model and/or Complementary Material or a Contribution incorporated within the Model and/or Complementary Material constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for the Model and/or Work shall terminate as of the date such litigation is asserted or filed. 33 | 34 | Section III: CONDITIONS OF USAGE, DISTRIBUTION AND REDISTRIBUTION 35 | 36 | 4. Distribution and Redistribution. You may host for Third Party remote access purposes (e.g. software-as-a-service), reproduce and distribute copies of the Model or Derivatives of the Model thereof in any medium, with or without modifications, provided that You meet the following conditions: 37 | Use-based restrictions as referenced in paragraph 5 MUST be included as an enforceable provision by You in any type of legal agreement (e.g. a license) governing the use and/or distribution of the Model or Derivatives of the Model, and You shall give notice to subsequent users You Distribute to, that the Model or Derivatives of the Model are subject to paragraph 5. This provision does not apply to the use of Complementary Material. 38 | You must give any Third Party recipients of the Model or Derivatives of the Model a copy of this License; 39 | You must cause any modified files to carry prominent notices stating that You changed the files; 40 | You must retain all copyright, patent, trademark, and attribution notices excluding those notices that do not pertain to any part of the Model, Derivatives of the Model. 41 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions - respecting paragraph 4.a. - for use, reproduction, or Distribution of Your modifications, or for any such Derivatives of the Model as a whole, provided Your use, reproduction, and Distribution of the Model otherwise complies with the conditions stated in this License. 42 | 5. Use-based restrictions. The restrictions set forth in Attachment A are considered Use-based restrictions. Therefore You cannot use the Model and the Derivatives of the Model for the specified restricted uses. You may use the Model subject to this License, including only for lawful purposes and in accordance with the License. Use may include creating any content with, finetuning, updating, running, training, evaluating and/or reparametrizing the Model. You shall require all of Your users who use the Model or a Derivative of the Model to comply with the terms of this paragraph (paragraph 5). 43 | 6. The Output You Generate. Except as set forth herein, Licensor claims no rights in the Output You generate using the Model. You are accountable for the Output you generate and its subsequent uses. No use of the output can contravene any provision as stated in the License. 44 | 45 | Section IV: OTHER PROVISIONS 46 | 47 | 7. Updates and Runtime Restrictions. To the maximum extent permitted by law, Licensor reserves the right to restrict (remotely or otherwise) usage of the Model in violation of this License. 48 | 8. Trademarks and related. Nothing in this License permits You to make use of Licensors’ trademarks, trade names, logos or to otherwise suggest endorsement or misrepresent the relationship between the parties; and any rights not expressly granted herein are reserved by the Licensors. 49 | 9. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Model and the Complementary Material (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Model, Derivatives of the Model, and the Complementary Material and assume any risks associated with Your exercise of permissions under this License. 50 | 10. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Model and the Complementary Material (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 51 | 11. Accepting Warranty or Additional Liability. While redistributing the Model, Derivatives of the Model and the Complementary Material thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 12. If any provision of this License is held to be invalid, illegal or unenforceable, the remaining provisions shall be unaffected thereby and remain valid as if such provision had not been set forth herein. 53 | 54 | END OF TERMS AND CONDITIONS 55 | 56 | 57 | 58 | 59 | Attachment A 60 | 61 | Use Restrictions 62 | 63 | You agree to use the Model or Derivatives of the Model: 64 | 65 | - Solely for research purposes -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniverSeg: Universal Medical Image Segmentation 2 | 3 | ### [Project Page](https://universeg.csail.mit.edu) | [Paper](http://arxiv.org/abs/2304.06131) 4 | 5 | [![Explore UniverSeg in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1TiNAgCehFdyHMJsS90V9ygUw0rLXdW0r?usp=sharing)
6 | 7 | Official implementation of ["UniverSeg: Universal Medical Image Segmentation"](http://arxiv.org/abs/2304.06131) accepted at ICCV 2023. 8 | 9 | [Victor Ion Butoi](https://victorbutoi.github.io)\*, 10 | [Jose Javier Gonzalez Ortiz](https://josejg.com)\*, 11 | [Tianyu Ma](https://www.linkedin.com/in/tianyu-ma-472219174/), 12 | [Mert R. Sabuncu](https://sabuncu.engineering.cornell.edu/), 13 | [John Guttag](https://people.csail.mit.edu/guttag/), 14 | [Adrian V. Dalca](http://www.mit.edu/~adalca/), 15 | \*denotes equal contribution 16 | 17 | 18 | 19 | ![network](https://raw.githubusercontent.com/JJGO/UniverSeg/gh-pages/assets/images/network-architecture.png) 20 | 21 | Given a new segmentation task (e.g. new biomedical domain, new image type, new region of interest, etc), most existing strategies involve training or fine-tuning a segmentation model that takes an image input and outputs the segmentation map. 22 | 23 | This process works well in machine-learning labs, but is challenging in many applied settings, such as for scientists or clinical researchers who drive important scientific questions, but often lack the machine-learning expertiese and computational resources necessary. 24 | 25 | UniverSeg enables users to tackle a new segmentation task **without the need to train or fine-tune a model**, removing the requirement for ML experience and computational burden. The key idea is to have a single global model which adapts to a new segmentation task at inference based on an input example set. 26 | 27 | 28 | 29 | ## Getting Started 30 | 31 | The universeg architecture is described in the [`model.py`](https://github.com/JJGO/UniverSeg/blob/main/universeg/model.py#L125) file. 32 | We provide model weights a part of our [release](https://github.com/JJGO/UniverSeg/releases/tag/weights). 33 | 34 | To instantiate the UniverSeg model (and optionally use provided weights): 35 | 36 | ```python 37 | from universeg import universeg 38 | 39 | model = universeg(pretrained=True) 40 | 41 | # To perform a prediction (where B=batch, S=support, H=height, W=width) 42 | prediction = model( 43 | target_image, # (B, 1, H, W) 44 | support_images, # (B, S, 1, H, W) 45 | support_labels, # (B, S, 1, H, W) 46 | ) # -> (B, 1, H, W) 47 | 48 | ``` 49 | 50 | For all inputs ensure that pixel values are min-max normalized to the $[0,1]$ range and that the spatial dimensions are $(H, W) = (128, 128)$. 51 | 52 | We provide a jupyter notebook with a tutorial and examples of how to do inference using UniverSeg: [Google colab](https://colab.research.google.com/drive/1TiNAgCehFdyHMJsS90V9ygUw0rLXdW0r?usp=sharing) | [Nbviewer](https://nbviewer.org/github/JJGO/UniverSeg/blob/gh-pages/jupyter/UniverSeg_demo.ipynb#). 53 | 54 | 55 | ## Installation 56 | 57 | You can install `universeg` in two ways: 58 | 59 | - **With pip**: 60 | 61 | ```shell 62 | pip install git+https://github.com/JJGO/UniverSeg.git 63 | ``` 64 | 65 | - **Manually**: Cloning it and installing dependencies 66 | 67 | ```shell 68 | git clone https://github.com/JJGO/UniverSeg 69 | python -m pip install -r ./UniverSeg/requirements.txt 70 | export PYTHONPATH="$PYTHONPATH:$(realpath ./UniverSeg)" 71 | ``` 72 | 73 | 74 | ## Citation 75 | 76 | If you find our work or any of our materials useful, please cite our paper: 77 | ``` 78 | @article{butoi2023universeg, 79 | title={UniverSeg: Universal Medical Image Segmentation}, 80 | author={Victor Ion Butoi* and Jose Javier Gonzalez Ortiz* and Tianyu Ma and Mert R. Sabuncu and John Guttag and Adrian V. Dalca}, 81 | journal={International Conference on Computer Vision}, 82 | year={2023} 83 | } 84 | ``` 85 | 86 | ## Licenses 87 | 88 | - **Code** is released under [Apache 2.0 license](LICENSE) 89 | - **Model Weights** are released under [OpenRAIL++-M license](LICENSE-model). According to the usage restriction the model must be only used for research purposes. 90 | -------------------------------------------------------------------------------- /example_data/oasis.py: -------------------------------------------------------------------------------- 1 | """ 2 | OASIS dataset processed at https://github.com/adalca/medical-datasets/blob/master/neurite-oasis.md 3 | """ 4 | 5 | import pathlib 6 | import subprocess 7 | from dataclasses import dataclass 8 | from typing import Literal, Tuple 9 | 10 | import numpy as np 11 | import nibabel as nib 12 | import PIL 13 | import torch 14 | from torch.utils.data import Dataset 15 | 16 | 17 | def process_img(path: pathlib.Path, size: Tuple[int, int]): 18 | img = (nib.load(path).get_fdata() * 255).astype(np.uint8).squeeze() 19 | img = PIL.Image.fromarray(img) 20 | img = img.resize(size, resample=PIL.Image.BILINEAR) 21 | img = img.convert("L") 22 | img = np.array(img) 23 | img = img.astype(np.float32)/255 24 | img = np.rot90(img, -1) 25 | return img.copy() 26 | 27 | 28 | def process_seg(path: pathlib.Path, size: Tuple[int, int]): 29 | seg = nib.load(path).get_fdata().astype(np.int8).squeeze() 30 | seg = PIL.Image.fromarray(seg) 31 | seg = seg.resize(size, resample=PIL.Image.NEAREST) 32 | seg = np.array(seg) 33 | seg = seg.astype(np.float32) 34 | seg = np.rot90(seg, -1) 35 | return seg.copy() 36 | 37 | 38 | def load_folder(path: pathlib.Path, size: Tuple[int, int] = (128, 128)): 39 | data = [] 40 | for file in sorted(path.glob("*/slice_norm.nii.gz")): 41 | img = process_img(file, size=size) 42 | seg_file = pathlib.Path(str(file).replace("slice_norm", "slice_seg24")) 43 | seg = process_seg(seg_file, size=size) 44 | data.append((img, seg)) 45 | return data 46 | 47 | 48 | def require_download_oasis(): 49 | dest_folder = pathlib.Path("/tmp/universeg_oasis/") 50 | 51 | if not dest_folder.exists(): 52 | tar_url = "https://surfer.nmr.mgh.harvard.edu/ftp/data/neurite/data/neurite-oasis.2d.v1.0.tar" 53 | subprocess.run( 54 | ["curl", tar_url, "--create-dirs", "-o", 55 | str(dest_folder/'neurite-oasis.2d.v1.0.tar'),], 56 | stderr=subprocess.DEVNULL, 57 | check=True, 58 | ) 59 | 60 | subprocess.run( 61 | ["tar", 'xf', str( 62 | dest_folder/'neurite-oasis.2d.v1.0.tar'), '-C', str(dest_folder)], 63 | stderr=subprocess.DEVNULL, 64 | check=True, 65 | ) 66 | 67 | return dest_folder 68 | 69 | 70 | @dataclass 71 | class OASISDataset(Dataset): 72 | split: Literal["support", "test"] 73 | label: int 74 | support_frac: float = 0.7 75 | 76 | def __post_init__(self): 77 | path = require_download_oasis() 78 | T = torch.from_numpy 79 | self._data = [(T(x)[None], T(y)) for x, y in load_folder(path)] 80 | if self.label is not None: 81 | self._ilabel = self.label 82 | self._idxs = self._split_indexes() 83 | 84 | def _split_indexes(self): 85 | rng = np.random.default_rng(42) 86 | N = len(self._data) 87 | p = rng.permutation(N) 88 | i = int(np.floor(self.support_frac * N)) 89 | return {"support": p[:i], "test": p[i:]}[self.split] 90 | 91 | def __len__(self): 92 | return len(self._idxs) 93 | 94 | def __getitem__(self, idx): 95 | img, seg = self._data[self._idxs[idx]] 96 | if self.label is not None: 97 | seg = (seg == self._ilabel)[None] 98 | return img, seg 99 | -------------------------------------------------------------------------------- /example_data/wbc.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import subprocess 3 | from dataclasses import dataclass 4 | from typing import Literal, Optional, Tuple 5 | 6 | import numpy as np 7 | import PIL 8 | import torch 9 | from torch.utils.data import Dataset 10 | 11 | 12 | def process_img(path: pathlib.Path, size: Tuple[int, int]): 13 | img = PIL.Image.open(path) 14 | img = img.resize(size, resample=PIL.Image.BILINEAR) 15 | img = img.convert("L") 16 | img = np.array(img) 17 | img = img.astype(np.float32) 18 | return img 19 | 20 | 21 | def process_seg(path: pathlib.Path, size: Tuple[int, int]): 22 | seg = PIL.Image.open(path) 23 | seg = seg.resize(size, resample=PIL.Image.NEAREST) 24 | seg = np.array(seg) 25 | seg = np.stack([seg == 0, seg == 128, seg == 255]) 26 | seg = seg.astype(np.float32) 27 | return seg 28 | 29 | 30 | def load_folder(path: pathlib.Path, size: Tuple[int, int] = (128, 128)): 31 | data = [] 32 | for file in sorted(path.glob("*.bmp")): 33 | img = process_img(file, size=size) 34 | seg_file = file.with_suffix(".png") 35 | seg = process_seg(seg_file, size=size) 36 | data.append((img / 255.0, seg)) 37 | return data 38 | 39 | 40 | def require_download_wbc(): 41 | dest_folder = pathlib.Path("/tmp/universeg_wbc/") 42 | 43 | if not dest_folder.exists(): 44 | repo_url = "https://github.com/zxaoyou/segmentation_WBC.git" 45 | subprocess.run( 46 | ["git", "clone", repo_url, str(dest_folder),], 47 | stderr=subprocess.DEVNULL, 48 | check=True, 49 | ) 50 | 51 | return dest_folder 52 | 53 | 54 | @dataclass 55 | class WBCDataset(Dataset): 56 | dataset: Literal["JTSC", "CV"] 57 | split: Literal["support", "test"] 58 | label: Optional[Literal["nucleus", "cytoplasm", "background"]] = None 59 | support_frac: float = 0.7 60 | 61 | def __post_init__(self): 62 | root = require_download_wbc() 63 | path = root / {"JTSC": "Dataset 1", "CV": "Dataset 2"}[self.dataset] 64 | T = torch.from_numpy 65 | self._data = [(T(x)[None], T(y)) for x, y in load_folder(path)] 66 | if self.label is not None: 67 | self._ilabel = {"cytoplasm": 1, "nucleus": 2, "background": 0}[self.label] 68 | self._idxs = self._split_indexes() 69 | 70 | def _split_indexes(self): 71 | rng = np.random.default_rng(42) 72 | N = len(self._data) 73 | p = rng.permutation(N) 74 | i = int(np.floor(self.support_frac * N)) 75 | return {"support": p[:i], "test": p[i:]}[self.split] 76 | 77 | def __len__(self): 78 | return len(self._idxs) 79 | 80 | def __getitem__(self, idx): 81 | img, seg = self._data[self._idxs[idx]] 82 | if self.label is not None: 83 | seg = seg[self._ilabel][None] 84 | return img, seg 85 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | einops 2 | torch 3 | pydantic 4 | numpy 5 | Pillow 6 | nibabel 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("requirements.txt") as f: 4 | requirements = f.read().splitlines() 5 | 6 | setup( 7 | name="universeg", 8 | version="0.1.0", 9 | packages=find_packages(), 10 | install_requires=requirements, 11 | ) 12 | -------------------------------------------------------------------------------- /universeg/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import UniverSeg, CrossBlock, CrossConv2d, universeg 2 | -------------------------------------------------------------------------------- /universeg/model.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Dict, List, Literal, Optional, Tuple, Union 3 | 4 | import einops as E 5 | import torch 6 | from torch import nn 7 | 8 | from .nn import CrossConv2d 9 | from .nn import reset_conv2d_parameters 10 | from .nn import Vmap, vmap 11 | from .validation import (Kwargs, as_2tuple, size2t, validate_arguments, 12 | validate_arguments_init) 13 | 14 | 15 | def get_nonlinearity(nonlinearity: Optional[str]) -> nn.Module: 16 | if nonlinearity is None: 17 | return nn.Identity() 18 | if nonlinearity == "Softmax": 19 | # For Softmax, we need to specify the channel dimension 20 | return nn.Softmax(dim=1) 21 | if hasattr(nn, nonlinearity): 22 | return getattr(nn, nonlinearity)() 23 | raise ValueError(f"nonlinearity {nonlinearity} not found") 24 | 25 | 26 | @validate_arguments_init 27 | @dataclass(eq=False, repr=False) 28 | class ConvOp(nn.Sequential): 29 | 30 | in_channels: int 31 | out_channels: int 32 | kernel_size: size2t = 3 33 | nonlinearity: Optional[str] = "LeakyReLU" 34 | init_distribution: Optional[str] = "kaiming_normal" 35 | init_bias: Union[None, float, int] = 0.0 36 | 37 | def __post_init__(self): 38 | super().__init__() 39 | self.conv = nn.Conv2d( 40 | self.in_channels, 41 | self.out_channels, 42 | kernel_size=self.kernel_size, 43 | padding=self.kernel_size // 2, 44 | padding_mode="zeros", 45 | bias=True, 46 | ) 47 | 48 | if self.nonlinearity is not None: 49 | self.nonlin = get_nonlinearity(self.nonlinearity) 50 | 51 | reset_conv2d_parameters( 52 | self, self.init_distribution, self.init_bias, self.nonlinearity 53 | ) 54 | 55 | 56 | @validate_arguments_init 57 | @dataclass(eq=False, repr=False) 58 | class CrossOp(nn.Module): 59 | 60 | in_channels: size2t 61 | out_channels: int 62 | kernel_size: size2t = 3 63 | nonlinearity: Optional[str] = "LeakyReLU" 64 | init_distribution: Optional[str] = "kaiming_normal" 65 | init_bias: Union[None, float, int] = 0.0 66 | 67 | def __post_init__(self): 68 | super().__init__() 69 | 70 | self.cross_conv = CrossConv2d( 71 | in_channels=as_2tuple(self.in_channels), 72 | out_channels=self.out_channels, 73 | kernel_size=self.kernel_size, 74 | padding=self.kernel_size // 2, 75 | ) 76 | 77 | if self.nonlinearity is not None: 78 | self.nonlin = get_nonlinearity(self.nonlinearity) 79 | 80 | reset_conv2d_parameters( 81 | self, self.init_distribution, self.init_bias, self.nonlinearity 82 | ) 83 | 84 | def forward(self, target, support): 85 | interaction = self.cross_conv(target, support).squeeze(dim=1) 86 | 87 | if self.nonlinearity is not None: 88 | interaction = vmap(self.nonlin, interaction) 89 | 90 | new_target = interaction.mean(dim=1, keepdims=True) 91 | 92 | return new_target, interaction 93 | 94 | 95 | @validate_arguments_init 96 | @dataclass(eq=False, repr=False) 97 | class CrossBlock(nn.Module): 98 | 99 | in_channels: size2t 100 | cross_features: int 101 | conv_features: Optional[int] = None 102 | cross_kws: Optional[Dict[str, Any]] = None 103 | conv_kws: Optional[Dict[str, Any]] = None 104 | 105 | def __post_init__(self): 106 | super().__init__() 107 | 108 | conv_features = self.conv_features or self.cross_features 109 | cross_kws = self.cross_kws or {} 110 | conv_kws = self.conv_kws or {} 111 | 112 | self.cross = CrossOp(self.in_channels, self.cross_features, **cross_kws) 113 | self.target = Vmap(ConvOp(self.cross_features, conv_features, **conv_kws)) 114 | self.support = Vmap(ConvOp(self.cross_features, conv_features, **conv_kws)) 115 | 116 | def forward(self, target, support): 117 | target, support = self.cross(target, support) 118 | target = self.target(target) 119 | support = self.support(support) 120 | return target, support 121 | 122 | 123 | @validate_arguments_init 124 | @dataclass(eq=False, repr=False) 125 | class UniverSeg(nn.Module): 126 | 127 | encoder_blocks: List[size2t] 128 | decoder_blocks: Optional[List[size2t]] = None 129 | 130 | def __post_init__(self): 131 | super().__init__() 132 | 133 | self.downsample = nn.MaxPool2d(2, 2) 134 | self.upsample = nn.UpsamplingBilinear2d(scale_factor=2) 135 | 136 | self.enc_blocks = nn.ModuleList() 137 | self.dec_blocks = nn.ModuleList() 138 | 139 | encoder_blocks = list(map(as_2tuple, self.encoder_blocks)) 140 | decoder_blocks = self.decoder_blocks or encoder_blocks[-2::-1] 141 | decoder_blocks = list(map(as_2tuple, decoder_blocks)) 142 | 143 | block_kws = dict(cross_kws=dict(nonlinearity=None)) 144 | 145 | in_ch = (1, 2) 146 | out_channels = 1 147 | out_activation = None 148 | 149 | # Encoder 150 | skip_outputs = [] 151 | for (cross_ch, conv_ch) in encoder_blocks: 152 | block = CrossBlock(in_ch, cross_ch, conv_ch, **block_kws) 153 | in_ch = conv_ch 154 | self.enc_blocks.append(block) 155 | skip_outputs.append(in_ch) 156 | 157 | # Decoder 158 | skip_chs = skip_outputs[-2::-1] 159 | for (cross_ch, conv_ch), skip_ch in zip(decoder_blocks, skip_chs): 160 | block = CrossBlock(in_ch + skip_ch, cross_ch, conv_ch, **block_kws) 161 | in_ch = conv_ch 162 | self.dec_blocks.append(block) 163 | 164 | self.out_conv = ConvOp( 165 | in_ch, out_channels, kernel_size=1, nonlinearity=out_activation, 166 | ) 167 | 168 | def forward(self, target_image, support_images, support_labels): 169 | 170 | target = E.rearrange(target_image, "B 1 H W -> B 1 1 H W") 171 | support = torch.cat([support_images, support_labels], dim=2) 172 | 173 | pass_through = [] 174 | 175 | for i, encoder_block in enumerate(self.enc_blocks): 176 | target, support = encoder_block(target, support) 177 | if i == len(self.encoder_blocks) - 1: 178 | break 179 | pass_through.append((target, support)) 180 | target = vmap(self.downsample, target) 181 | support = vmap(self.downsample, support) 182 | 183 | for decoder_block in self.dec_blocks: 184 | target_skip, support_skip = pass_through.pop() 185 | target = torch.cat([vmap(self.upsample, target), target_skip], dim=2) 186 | support = torch.cat([vmap(self.upsample, support), support_skip], dim=2) 187 | target, support = decoder_block(target, support) 188 | 189 | target = E.rearrange(target, "B 1 C H W -> B C H W") 190 | target = self.out_conv(target) 191 | 192 | return target 193 | 194 | 195 | @validate_arguments 196 | def universeg(version: Literal["v1"] = "v1", pretrained: bool = False) -> nn.Module: 197 | weights = { 198 | "v1": "https://github.com/JJGO/UniverSeg/releases/download/weights/universeg_v1_nf64_ss64_STA.pt" 199 | } 200 | 201 | if version == "v1": 202 | model = UniverSeg(encoder_blocks=[64, 64, 64, 64]) 203 | 204 | if pretrained: 205 | state_dict = torch.hub.load_state_dict_from_url(weights[version]) 206 | model.load_state_dict(state_dict) 207 | 208 | return model 209 | -------------------------------------------------------------------------------- /universeg/nn/__init__.py: -------------------------------------------------------------------------------- 1 | from .cross_conv import CrossConv2d 2 | from .init import reset_conv2d_parameters 3 | from .vmap import vmap, Vmap -------------------------------------------------------------------------------- /universeg/nn/cross_conv.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple, Union 2 | 3 | import einops as E 4 | import torch 5 | import torch.nn as nn 6 | from pydantic import validate_arguments 7 | 8 | size2t = Union[int, Tuple[int, int]] 9 | 10 | 11 | class CrossConv2d(nn.Conv2d): 12 | """ 13 | Compute pairwise convolution between all element of x and all elements of y. 14 | x, y are tensors of size B,_,C,H,W where _ could be different number of elements in x and y 15 | essentially, we do a meshgrid of the elements to get B,Sx,Sy,C,H,W tensors, and then 16 | pairwise conv. 17 | Args: 18 | x (tensor): B,Sx,Cx,H,W 19 | y (tensor): B,Sy,Cy,H,W 20 | Returns: 21 | tensor: B,Sx,Sy,Cout,H,W 22 | """ 23 | """ 24 | CrossConv2d is a convolutional layer that performs pairwise convolutions between elements of two input tensors. 25 | 26 | Parameters 27 | ---------- 28 | in_channels : int or tuple of ints 29 | Number of channels in the input tensor(s). 30 | If the tensors have different number of channels, in_channels must be a tuple 31 | out_channels : int 32 | Number of output channels. 33 | kernel_size : int or tuple of ints 34 | Size of the convolutional kernel. 35 | stride : int or tuple of ints, optional 36 | Stride of the convolution. Default is 1. 37 | padding : int or tuple of ints, optional 38 | Zero-padding added to both sides of the input. Default is 0. 39 | dilation : int or tuple of ints, optional 40 | Spacing between kernel elements. Default is 1. 41 | groups : int, optional 42 | Number of blocked connections from input channels to output channels. Default is 1. 43 | bias : bool, optional 44 | If True, adds a learnable bias to the output. Default is True. 45 | padding_mode : str, optional 46 | Padding mode. Default is "zeros". 47 | device : str, optional 48 | Device on which to allocate the tensor. Default is None. 49 | dtype : torch.dtype, optional 50 | Data type assigned to the tensor. Default is None. 51 | 52 | Returns 53 | ------- 54 | torch.Tensor 55 | Tensor resulting from the pairwise convolution between the elements of x and y. 56 | 57 | Notes 58 | ----- 59 | x and y are tensors of size (B, Sx, Cx, H, W) and (B, Sy, Cy, H, W), respectively, 60 | The function does the cartesian product of the elements of x and y to obtain a tensor 61 | of size (B, Sx, Sy, Cx + Cy, H, W), and then performs the same convolution for all 62 | (B, Sx, Sy) in the batch dimension. Runtime and memory are O(Sx * Sy). 63 | 64 | Examples 65 | -------- 66 | >>> x = torch.randn(2, 3, 4, 32, 32) 67 | >>> y = torch.randn(2, 5, 6, 32, 32) 68 | >>> conv = CrossConv2d(in_channels=(4, 6), out_channels=7, kernel_size=3, padding=1) 69 | >>> output = conv(x, y) 70 | >>> output.shape #(2, 3, 5, 7, 32, 32) 71 | """ 72 | 73 | @validate_arguments 74 | def __init__( 75 | self, 76 | in_channels: size2t, 77 | out_channels: int, 78 | kernel_size: size2t, 79 | stride: size2t = 1, 80 | padding: size2t = 0, 81 | dilation: size2t = 1, 82 | groups: int = 1, 83 | bias: bool = True, 84 | padding_mode: str = "zeros", 85 | device=None, 86 | dtype=None, 87 | ) -> None: 88 | 89 | if isinstance(in_channels, (list, tuple)): 90 | concat_channels = sum(in_channels) 91 | else: 92 | concat_channels = 2 * in_channels 93 | 94 | super().__init__( 95 | in_channels=concat_channels, 96 | out_channels=out_channels, 97 | kernel_size=kernel_size, 98 | stride=stride, 99 | padding=padding, 100 | dilation=dilation, 101 | groups=groups, 102 | bias=bias, 103 | padding_mode=padding_mode, 104 | device=device, 105 | dtype=dtype, 106 | ) 107 | 108 | def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: 109 | """ 110 | Compute pairwise convolution between all elements of x and all elements of y. 111 | 112 | Parameters 113 | ---------- 114 | x : torch.Tensor 115 | Input tensor of size (B, Sx, Cx, H, W). 116 | y : torch.Tensor 117 | Input tensor of size (B, Sy, Cy, H, W). 118 | 119 | Returns 120 | ------- 121 | torch.Tensor 122 | Tensor resulting from the cross-convolution between the elements of x and y. 123 | Has size (B, Sx, Sy, Co, H, W), where Co is the number of output channels. 124 | """ 125 | B, Sx, *_ = x.shape 126 | _, Sy, *_ = y.shape 127 | 128 | xs = E.repeat(x, "B Sx Cx H W -> B Sx Sy Cx H W", Sy=Sy) 129 | ys = E.repeat(y, "B Sy Cy H W -> B Sx Sy Cy H W", Sx=Sx) 130 | 131 | xy = torch.cat([xs, ys], dim=3,) 132 | 133 | batched_xy = E.rearrange(xy, "B Sx Sy C2 H W -> (B Sx Sy) C2 H W") 134 | batched_output = super().forward(batched_xy) 135 | 136 | output = E.rearrange( 137 | batched_output, "(B Sx Sy) Co H W -> B Sx Sy Co H W", B=B, Sx=Sx, Sy=Sy 138 | ) 139 | return output 140 | -------------------------------------------------------------------------------- /universeg/nn/init.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Optional 3 | 4 | import torch 5 | from torch import nn 6 | from torch.nn import init 7 | 8 | 9 | def initialize_weight( 10 | weight: torch.Tensor, 11 | distribution: Optional[str], 12 | nonlinearity: Optional[str] = "LeakyReLU", 13 | ) -> None: 14 | """Initialize the weight tensor with a chosen distribution and nonlinearity. 15 | 16 | Args: 17 | weight (torch.Tensor): The weight tensor to initialize. 18 | distribution (Optional[str]): The distribution to use for initialization. Can be one of "zeros", 19 | "kaiming_normal", "kaiming_uniform", "kaiming_normal_fanout", "kaiming_uniform_fanout", 20 | "glorot_normal", "glorot_uniform", or "orthogonal". 21 | nonlinearity (Optional[str]): The type of nonlinearity to use. Can be one of "LeakyReLU", "Sine", 22 | "Tanh", "Silu", or "Gelu". 23 | 24 | Returns: 25 | None 26 | """ 27 | 28 | if distribution is None: 29 | return 30 | 31 | if nonlinearity: 32 | nonlinearity = nonlinearity.lower() 33 | if nonlinearity == "leakyrelu": 34 | nonlinearity = "leaky_relu" 35 | 36 | if nonlinearity == "sine": 37 | warnings.warn("sine gain not implemented, defaulting to tanh") 38 | nonlinearity = "tanh" 39 | 40 | if nonlinearity is None: 41 | nonlinearity = "linear" 42 | 43 | if nonlinearity in ("silu", "gelu"): 44 | nonlinearity = "leaky_relu" 45 | 46 | gain = 1 if nonlinearity is None else init.calculate_gain(nonlinearity) 47 | 48 | if distribution == "zeros": 49 | init.zeros_(weight) 50 | elif distribution == "kaiming_normal": 51 | init.kaiming_normal_(weight, nonlinearity=nonlinearity) 52 | elif distribution == "kaiming_uniform": 53 | init.kaiming_uniform_(weight, nonlinearity=nonlinearity) 54 | elif distribution == "kaiming_normal_fanout": 55 | init.kaiming_normal_(weight, nonlinearity=nonlinearity, mode="fan_out") 56 | elif distribution == "kaiming_uniform_fanout": 57 | init.kaiming_uniform_(weight, nonlinearity=nonlinearity, mode="fan_out") 58 | elif distribution == "glorot_normal": 59 | init.xavier_normal_(weight, gain=gain) 60 | elif distribution == "glorot_uniform": 61 | init.xavier_uniform_(weight, gain) 62 | elif distribution == "orthogonal": 63 | init.orthogonal_(weight, gain) 64 | else: 65 | raise ValueError(f"Unsupported distribution '{distribution}'") 66 | 67 | 68 | def initialize_bias( 69 | bias: torch.Tensor, 70 | distribution: Optional[float] = 0, 71 | nonlinearity: Optional[str] = "LeakyReLU", 72 | weight: Optional[torch.Tensor] = None, 73 | ) -> None: 74 | """Initialize the bias tensor with a constant or a chosen distribution and nonlinearity. 75 | 76 | Args: 77 | bias (torch.Tensor): The bias tensor to initialize. 78 | distribution (Optional[float]): The constant value to initialize the bias to. 79 | nonlinearity (Optional[str]): The type of nonlinearity to use when initializing the bias. 80 | weight (Optional[torch.Tensor]): The weight tensor to use when initializing the bias. 81 | 82 | Returns: 83 | None 84 | """ 85 | 86 | if distribution is None: 87 | return 88 | 89 | if isinstance(distribution, (int, float)): 90 | init.constant_(bias, distribution) 91 | else: 92 | raise NotImplementedError(f"Unsupported distribution '{distribution}'") 93 | 94 | 95 | def initialize_layer( 96 | layer: nn.Module, 97 | distribution: Optional[str] = "kaiming_normal", 98 | init_bias: Optional[float] = 0, 99 | nonlinearity: Optional[str] = "LeakyReLU", 100 | ) -> None: 101 | """Initialize the weight and bias tensors of a linear or convolutional layer. 102 | 103 | Args: 104 | layer (nn.Module): The layer to initialize. 105 | distribution (Optional[str]): The distribution to use for weight initialization. 106 | init_bias (Optional[float]): The value to use for bias initialization. 107 | nonlinearity (Optional[str]): The type of nonlinearity to use when initializing the layer. 108 | 109 | Returns: 110 | None 111 | """ 112 | 113 | assert isinstance( 114 | layer, (nn.Linear, nn.Conv1d, nn.Conv2d, nn.Conv3d) 115 | ), f"Can only be applied to linear and conv layers, given {layer.__class__.__name__}" 116 | 117 | initialize_weight(layer.weight, distribution, nonlinearity) 118 | if layer.bias is not None: 119 | initialize_bias( 120 | layer.bias, init_bias, nonlinearity=nonlinearity, weight=layer.weight 121 | ) 122 | 123 | 124 | def reset_conv2d_parameters( 125 | model: nn.Module, 126 | init_distribution: Optional[str], 127 | init_bias: Optional[float], 128 | nonlinearity: Optional[str], 129 | ) -> None: 130 | """Reset the parameters of all convolutional layers in the model. 131 | 132 | Args: 133 | model (nn.Module): The model to reset the convolutional layers of. 134 | init_distribution (Optional[str]): The distribution to use for weight initialization. 135 | init_bias (Optional[float]): The value to use for bias initialization. 136 | nonlinearity (Optional[str]): The type of nonlinearity to use when initializing the layers. 137 | 138 | Returns: 139 | None 140 | """ 141 | 142 | for name, module in model.named_modules(): 143 | if isinstance(module, nn.Conv2d): 144 | initialize_layer( 145 | module, 146 | distribution=init_distribution, 147 | init_bias=init_bias, 148 | nonlinearity=nonlinearity, 149 | ) 150 | -------------------------------------------------------------------------------- /universeg/nn/vmap.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import einops as E 4 | import torch 5 | from torch import nn 6 | 7 | 8 | def vmap(module: Callable, x: torch.Tensor, *args, **kwargs) -> torch.Tensor: 9 | """ 10 | Applies the given module over the initial batch dimension and the second (group) dimension. 11 | 12 | Args: 13 | module: a callable that is applied over the batch dimension and the second (group) dimension. 14 | must support batch operations 15 | x: tensor of shape (batch_size, group_size, ...). 16 | args: positional arguments to pass to `module`. 17 | kwargs: keyword arguments to pass to `module`. 18 | 19 | Returns: 20 | The output tensor with the same shape as the input tensor. 21 | """ 22 | batch_size, group_size, *_ = x.shape 23 | grouped_input = E.rearrange(x, "B S ... -> (B S) ...") 24 | grouped_output = module(grouped_input, *args, **kwargs) 25 | output = E.rearrange( 26 | grouped_output, "(B S) ... -> B S ...", B=batch_size, S=group_size 27 | ) 28 | return output 29 | 30 | 31 | def vmap_fn(fn: Callable) -> Callable: 32 | """ 33 | Returns a callable that applies the input function over the initial batch dimension and the second (group) dimension. 34 | 35 | Args: 36 | fn: function to apply over the batch dimension and the second (group) dimension. 37 | 38 | Returns: 39 | A callable that applies the input function over the initial batch dimension and the second (group) dimension. 40 | """ 41 | 42 | def vmapped_fn(*args, **kwargs): 43 | return vmap(fn, *args, **kwargs) 44 | 45 | return vmapped_fn 46 | 47 | 48 | class Vmap(nn.Module): 49 | def __init__(self, module: nn.Module): 50 | """ 51 | Applies the given module over the initial batch dimension and the second (group) dimension. 52 | 53 | Args: 54 | module: module to apply over the batch dimension and the second (group) dimension. 55 | """ 56 | super().__init__() 57 | self.vmapped = module 58 | 59 | def forward(self, x: torch.Tensor) -> torch.Tensor: 60 | """ 61 | Applies the given module over the initial batch dimension and the second (group) dimension. 62 | 63 | Args: 64 | x: tensor of shape (batch_size, group_size, ...). 65 | 66 | Returns: 67 | The output tensor with the same shape as the input tensor. 68 | """ 69 | return vmap(self.vmapped, x) 70 | 71 | 72 | def vmap_cls(module_type: type) -> Callable: 73 | """ 74 | Returns a callable that applies the input module type over the initial batch dimension and the second (group) dimension. 75 | 76 | Args: 77 | module_type: module type to apply over the batch dimension and the second (group) dimension. 78 | 79 | Returns: 80 | A callable that applies the input module type over the initial batch dimension and the second (group) dimension. 81 | """ 82 | 83 | def vmapped_cls(*args, **kwargs): 84 | module = module_type(*args, **kwargs) 85 | return Vmap(module) 86 | 87 | return vmapped_cls 88 | -------------------------------------------------------------------------------- /universeg/validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module containing utility functions for validating arguments using Pydantic. 3 | 4 | Functions: 5 | - as_2tuple(val: size2t) -> Tuple[int, int]: Convert integer or 2-tuple to 2-tuple format. 6 | - validate_arguments_init(class_) -> class_: Decorator to validate the arguments of the __init__ method using Pydantic. 7 | """ 8 | 9 | from typing import Any, Dict, Tuple, Union 10 | 11 | from pydantic import validate_arguments 12 | 13 | size2t = Union[int, Tuple[int, int]] 14 | Kwargs = Dict[str, Any] 15 | 16 | 17 | def as_2tuple(val: size2t) -> Tuple[int, int]: 18 | """ 19 | Convert integer or 2-tuple to 2-tuple format. 20 | 21 | Args: 22 | val (Union[int, Tuple[int, int]]): The value to convert. 23 | 24 | Returns: 25 | Tuple[int, int]: The converted 2-tuple. 26 | 27 | Raises: 28 | AssertionError: If val is not an integer or a 2-tuple with length 2. 29 | """ 30 | if isinstance(val, int): 31 | return (val, val) 32 | assert isinstance(val, (list, tuple)) and len(val) == 2 33 | return tuple(val) 34 | 35 | 36 | def validate_arguments_init(class_): 37 | """ 38 | Decorator to validate the arguments of the __init__ method using Pydantic. 39 | 40 | Args: 41 | class_ (Any): The class to decorate. 42 | 43 | Returns: 44 | class_: The decorated class with validated __init__ method. 45 | """ 46 | class_.__init__ = validate_arguments(class_.__init__) 47 | return class_ 48 | --------------------------------------------------------------------------------