Package: class-resolver
Class ResolverLookup and instantiate classes with style.💪 Getting Startedfromclass_resolverimportClassResolverfromdataclassesimportdataclassclassBase:pass@dataclassclassA(Base):name:str@dataclassclassB(Base):name:str# Indexresolver=ClassResolver([A,B],base=Base)# LookupassertA==resolver.lookup('A')# Instantiate with a dictionaryassertA(name='hi')==resolver.make('A',{'name':'hi'})# Instantiate with kwargsassertA(name='hi')==resolver.make('A',name='hi')# A pre-instantiated class will simply be passed throughassertA(name='hi')==resolver.make(A(name='hi'))🤖 Writing Extensible Machine Learning Models withclass-resolverAssume you've implemented a simple multi-layer perceptron in PyTorch:fromitertoolsimportchainfrommore_itertoolsimportpairwisefromtorchimportnnclassMLP(nn.Sequential):def__init__(self,dims:list[int]):super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),nn.ReLU(),)forin_features,out_featuresinpairwise(dims)))This MLP uses a hard-coded rectified linear unit as the non-linear activation
function between layers. We can generalize this MLP to use a variety of
non-linear activation functions by adding an argument to its__init__()function like in:fromitertoolsimportchainfrommore_itertoolsimportpairwisefromtorchimportnnclassMLP(nn.Sequential):def__init__(self,dims:list[int],activation:str="relu"):ifactivation=="relu":activation=nn.ReLU()elifactivation=="tanh":activation=nn.Tanh()elifactivation=="hardtanh":activation=nn.Hardtanh()else:raiseKeyError(f"Unsupported activation:{activation}")super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),activation,)forin_features,out_featuresinpairwise(dims)))The first issue with this implementation is it relies on a hard-coded set of
conditional statements and is therefore hard to extend. It can be improved
by using a dictionary lookup:fromitertoolsimportchainfrommore_itertoolsimportpairwisefromtorchimportnnactivation_lookup:dict[str,nn.Module]={"relu":nn.ReLU(),"tanh":nn.Tanh(),"hardtanh":nn.Hardtanh(),}classMLP(nn.Sequential):def__init__(self,dims:list[int],activation:str="relu"):activation=activation_lookup[activation]super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),activation,)forin_features,out_featuresinpairwise(dims)))This approach is rigid because it requires pre-instantiation of the activations.
If we needed to vary the arguments to thenn.HardTanhclass, the previous
approach wouldn't work. We can change the implementation to lookup on the
classbefore instantiationthen optionally pass some arguments:fromitertoolsimportchainfrommore_itertoolsimportpairwisefromtorchimportnnactivation_lookup:dict[str,type[nn.Module]]={"relu":nn.ReLU,"tanh":nn.Tanh,"hardtanh":nn.Hardtanh,}classMLP(nn.Sequential):def__init__(self,dims:list[int],activation:str="relu",activation_kwargs:None|dict[str,any]=None,):activation_cls=activation_lookup[activation]activation=activation_cls(**(activation_kwargsor{}))super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),activation,)forin_features,out_featuresinpairwise(dims)))This is pretty good, but it still has a few issues:you have to manually maintain theactivation_lookupdictionary,you can't pass an instance or class through theactivationkeywordyou have to get the casing just rightthe default is hard-coded as a string, which means this has to get copied
(error-prone) in any place that creates an MLPyou have to re-write this logic for all of your classesEnter theclass_resolverpackage, which takes care of all of these
things using the following:fromitertoolsimportchainfromclass_resolverimportClassResolver,Hintfrommore_itertoolsimportpairwisefromtorchimportnnactivation_resolver=ClassResolver([nn.ReLU,nn.Tanh,nn.Hardtanh],base=nn.Module,default=nn.ReLU,)classMLP(nn.Sequential):def__init__(self,dims:list[int],activation:Hint[nn.Module]=None,# Hint = Union[None, str, nn.Module, type[nn.Module]]activation_kwargs:None|dict[str,any]=None,):super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),activation_resolver.make(activation,activation_kwargs),)forin_features,out_featuresinpairwise(dims)))Because this is such a common pattern, we've made it available through contrib
module inclass_resolver.contrib.torch:fromitertoolsimportchainfromclass_resolverimportHintfromclass_resolver.contrib.torchimportactivation_resolverfrommore_itertoolsimportpairwisefromtorchimportnnclassMLP(nn.Sequential):def__init__(self,dims:list[int],activation:Hint[nn.Module]=None,activation_kwargs:None|dict[str,any]=None,):super().__init__(chain.from_iterable((nn.Linear(in_features,out_features),activation_resolver.make(activation,activation_kwargs),)forin_features,out_featuresinpairwise(dims)))Now, you can instantiate the MLP with any of the following:MLP(dims=[10,200,40])# uses default, which is ReLUMLP(dims=[10,200,40],activation="relu")# uses lowercaseMLP(dims=[10,200,40],activation="ReLU")# uses stylizedMLP(dims=[10,200,40],activation=nn.ReLU)# uses classMLP(dims=[10,200,40],activation=nn.ReLU())# uses instanceMLP(dims=[10,200,40],activation="hardtanh",activation_kwargs={"min_val":0.0,"max_value":6.0})# uses kwargsMLP(dims=[10,200,40],activation=nn.HardTanh,activation_kwargs={"min_val":0.0,"max_value":6.0})# uses kwargsMLP(dims=[10,200,40],activation=nn.HardTanh(0.0,6.0))# uses instanceIn practice, it makes sense to stick to using the strings in combination with
hyper-parameter optimization libraries likeOptuna.⬇️ InstallationThe most recent release can be installed fromPyPIwith:$pipinstallclass_resolverThe most recent code and data can be installed directly from GitHub with:$pipinstallgit+https://github.com/cthoyt/class-resolver.gitTo install in development mode, use the following:$gitclonegit+https://github.com/cthoyt/class-resolver.git
$cdclass-resolver
$pipinstall-e.🙏 ContributingContributions, whether filing an issue, making a pull request, or forking, are appreciated. SeeCONTRIBUTING.rstfor more
information on getting involved.👋 Attribution⚖️ LicenseThe code in this package is licensed under the MIT License.🍪 CookiecutterThis package was created with@audreyfeldroy'scookiecutterpackage using@cthoyt'scookiecutter-snekpacktemplate.🛠️ For DevelopersSee developer instructionsThe final section of the README is for if you want to get involved by making a code contribution.❓ TestingAfter cloning the repository and installingtoxwithpip install tox, the unit tests in thetests/folder can be
run reproducibly with:$toxAdditionally, these tests are automatically re-run with each commit in aGitHub Action.📦 Making a ReleaseAfter installing the package in development mode and installingtoxwithpip install tox, the commands for making a new release are contained within thefinishenvironment
intox.ini. Run the following from the shell:$tox-efinishThis script does the following:Uses BumpVersion to switch the version number in thesetup.cfgandsrc/{{cookiecutter.package_name}}/version.pyto not have the-devsuffixPackages the code in both a tar archive and a wheelUploads to PyPI usingtwine. Be sure to have a.pypircfile configured to avoid the need for manual input at this
stepPush to GitHub. You'll need to make a release going with the commit where the version was bumped.Bump the version to the next patch. If you made big changes and want to bump the version by minor, you can
usetox -e bumpversion minorafter.