Browse Source

removed dependency on the chainer_addons package

Dimitri Korsch 2 years ago
parent
commit
f6abdbf147

+ 2 - 2
cvmodelz/classifiers/base.py

@@ -108,8 +108,8 @@ class Classifier(chainer.Chain):
 	def output_size(self) -> int:
 		return self.feat_size
 
-	def loss(self, pred: chainer.Variable, y: chainer.Variable) -> chainer.Variable:
-		return self.model.loss(pred, y, loss_func=self.loss_func)
+	def loss(self, pred: chainer.Variable, y: chainer.Variable, **kwargs) -> chainer.Variable:
+		return self.model.loss(pred, y, loss_func=self.loss_func, **kwargs)
 
 	def evaluations(self, pred: chainer.Variable, y: chainer.Variable) -> Dict[str, chainer.Variable]:
 		return dict(accuracy=self.model.accuracy(pred, y))

+ 3 - 3
cvmodelz/models/base.py

@@ -7,12 +7,12 @@ from chainer import functions as F
 from chainer import links as L
 from chainer.initializers import HeNormal
 from chainer.serializers import npz
-from chainer_addons.links.pooling import PoolingType # TODO: replace this!
 from collections import OrderedDict
 from typing import Callable
 
 from cvmodelz import utils
 from cvmodelz.models.meta_info import ModelInfo
+from cvmodelz.utils.links.pooling import PoolingType
 
 class BaseModel(abc.ABC, chainer.Chain):
 
@@ -54,8 +54,8 @@ class BaseModel(abc.ABC, chainer.Chain):
 	def clf_layer(self) -> chainer.Link:
 		return utils.get_attr_from_path(self.model_instance, self.clf_layer_name)
 
-	def loss(self, pred, gt, loss_func=F.softmax_cross_entropy):
-		return loss_func(pred, gt)
+	def loss(self, pred, gt, *, loss_func=F.softmax_cross_entropy, **kwargs):
+		return loss_func(pred, gt, **kwargs)
 
 	def accuracy(self, pred, gt):
 		return F.accuracy(pred, gt)

+ 1 - 1
cvmodelz/models/meta_info.py

@@ -21,7 +21,7 @@ class ModelInfo(object):
 
 	classifier_layers:          Tuple[str]  = ("fc",)
 
-	prepare_func:               Callable    = lambda x: x
+	prepare_func:               Callable    = lambda x: x  # noqa: E731
 
 	def prepare(self, foo):
 		pass

+ 0 - 2
cvmodelz/models/pretrained/base.py

@@ -1,8 +1,6 @@
-import abc
 from chainer import links as L
 
 from cvmodelz.models.base import BaseModel
-from cvmodelz.models.meta_info import ModelInfo
 
 class PretrainedModelMixin(BaseModel):
 	"""

+ 2 - 2
cvmodelz/models/pretrained/inception/blocks.py

@@ -2,8 +2,8 @@ import chainer
 import chainer.functions as F
 import chainer.links as L
 
-from chainer_addons.links import Conv2D_BN
-from chainer_addons.links.pooling import PoolingType
+from cvmodelz.utils.links import Conv2D_BN
+from cvmodelz.utils.links.pooling import PoolingType
 
 
 class InceptionHead(chainer.Chain):

+ 4 - 4
cvmodelz/models/pretrained/inception/inception_v3.py

@@ -3,7 +3,6 @@ import chainer.functions as F
 import chainer.links as L
 import numpy as np
 
-from chainer_addons.links.pooling import PoolingType # TODO: replace this!
 from chainercv.transforms import resize
 from chainercv.transforms import scale
 from collections import OrderedDict
@@ -14,6 +13,7 @@ from cvmodelz.models.meta_info import ModelInfo
 from cvmodelz.models.pretrained.base import PretrainedModelMixin
 from cvmodelz.models.pretrained.inception import blocks
 from cvmodelz.models.pretrained.inception import link_mappings
+from cvmodelz.utils.links.pooling import PoolingType
 
 
 def _assign(name, param, data):
@@ -193,17 +193,17 @@ class InceptionV3(PretrainedModelMixin, InceptionV3Layers):
 		# the final fc layer is initilized by PretrainedModelMixin
 		super().init_extra_layers(n_classes)
 
-	def loss(self, pred, gt, loss_func=F.softmax_cross_entropy, alpha=0.4):
+	def loss(self, pred, gt, *, loss_func=F.softmax_cross_entropy, alpha=0.4, **kwargs):
 		if isinstance(pred, tuple):
 			pred0, aux_pred = pred
 		else:
 			pred0, aux_pred = pred, None
 
-		loss = loss_func(pred0, gt)
+		loss = loss_func(pred0, gt, **kwargs)
 		if aux_pred is None:
 			return loss
 		else:
-			aux_loss = loss_func(aux_pred, gt)
+			aux_loss = loss_func(aux_pred, gt, **kwargs)
 			return (1-alpha) * loss + alpha * aux_loss
 
 	def accuracy(self, pred, gt):

+ 0 - 1
cvmodelz/models/wrapper.py

@@ -1,6 +1,5 @@
 import chainer
 
-from chainer import functions as F
 from collections import OrderedDict
 
 from cvmodelz.models.base import BaseModel

+ 10 - 0
cvmodelz/utils/functions/__init__.py

@@ -0,0 +1,10 @@
+from cvmodelz.utils.functions.alpha_product import alpha_prod
+from cvmodelz.utils.functions.signed_square_root import signed_square_root
+from cvmodelz.utils.functions.tf_average import tf_average_pooling_2d
+
+
+__all__ = [
+	"alpha_prod",
+	"signed_square_root",
+	"tf_average_pooling_2d"
+]

+ 60 - 0
cvmodelz/utils/functions/alpha_product.py

@@ -0,0 +1,60 @@
+import numpy as np
+
+from chainer.function import Function
+from chainer import cuda
+
+class AlphaProduct(Function):
+	"""
+		Implementation with the assumption that all inputs are positive
+
+		as the consequence following holds:
+		- x == abs(x)
+		- sign(x) == sign(x)**2
+		- the gradient for all x == 0 is 0 as well
+		- sign(x) * <anything> == <anything> for all x != 0
+
+		hence we only compute gradient for x != 0
+	"""
+	def forward(self, inputs):
+		xp = cuda.get_array_module(*inputs)
+		x, alpha = inputs
+		return xp.power(x, alpha - 1),
+
+	def backward_cpu(self, inputs, gys):
+		x, alpha = inputs
+		gy = gys[0]
+
+		mask = x != 0
+		gx = np.zeros_like(gy)
+		ga = np.zeros_like(gy)
+
+		if mask.any():
+			gx[mask] = np.power(x[mask], alpha - 2) * (alpha - 1)
+			ga[mask] = np.power(x[mask], alpha - 1) * np.log(x[mask])
+
+		gx = gx*gy
+		ga = (ga*gy).sum().reshape(1)
+		return gx, ga
+
+	def backward_gpu(self, inputs, gys):
+		x, alpha = inputs
+
+		gx = cuda.cupy.zeros_like(gys[0])
+		ga = cuda.cupy.zeros_like(gys[0])
+
+		cuda.elementwise(
+			in_params="B mask, T x, T alpha, T gy",
+			out_params="T gx, T ga",
+			operation="""
+				gx = mask ? 0 : gy * pow(x, alpha - 2) * (alpha - 1) ;
+				ga = mask ? 0 : gy * pow(x, alpha - 1) * log(x) ;
+			""",
+			name="alpha_prod2_bwd")(
+				x==0, x, alpha, gys[0],
+				gx, ga)
+
+		return gx, ga.sum().reshape(1)
+
+
+def alpha_prod(x, alpha, eps=1e-5):
+	return AlphaProduct()(x, alpha)

+ 36 - 0
cvmodelz/utils/functions/signed_square_root.py

@@ -0,0 +1,36 @@
+import numpy as np
+
+from chainer.function import Function
+from chainer import cuda
+
+class SignedSquareRoot(Function):
+
+	def forward(self, inputs):
+		xp = cuda.get_array_module(*inputs)
+		x, = inputs
+		return xp.sign(x) * xp.sqrt(xp.abs(x)),
+
+	def backward_cpu(self, inputs, gys):
+		x, gy = inputs[0], gys[0]#.copy()
+		gx = np.zeros_like(gy)
+
+		mask = x!=0
+		gx[mask] = gy[mask] / (2 * np.sqrt(np.abs(x[mask])))
+		return gx,
+
+	def backward_gpu(self, inputs, gys):
+
+		x = inputs[0]
+		gx = cuda.elementwise(
+			in_params="B mask, T x, T gy",
+			out_params="T gx",
+			operation="""
+				gx = mask ? 0 : gy / (2 * sqrt(abs(x)));
+			""",
+			name="signed_square_root_bwd")(x==0, x, gys[0])
+
+		return gx,
+
+
+def signed_square_root(x):
+	return SignedSquareRoot()(x)

+ 30 - 0
cvmodelz/utils/functions/tf_average.py

@@ -0,0 +1,30 @@
+import chainer
+
+from chainer.functions.pooling.average_pooling_2d import AveragePooling2D
+from chainer import cuda
+
+
+class TFAveragePooling2D(AveragePooling2D):
+	"""
+		The only thing, that changes is the CuDNN poolgin option from
+			CUDNN_POOLING_AVERAGE_COUNT_INCLUDE_PADDING
+		to
+			CUDNN_POOLING_AVERAGE_COUNT_EXCLUDE_PADDING
+
+		Note: CuDNN is required for this Function
+	"""
+
+	def __init__(self, *args, **kwargs):
+		super(TFAveragePooling2D, self).__init__(*args, **kwargs)
+
+
+	def create_pool_desc(self):
+		assert chainer.should_use_cudnn('>=auto'), \
+			"This function works only with CuDNN!"
+		return cuda.cudnn.create_pooling_descriptor(
+			(self.kh, self.kw), (self.sy, self.sx), (self.ph, self.pw),
+			cuda.cuda.cudnn.CUDNN_POOLING_AVERAGE_COUNT_EXCLUDE_PADDING)
+
+
+def tf_average_pooling_2d(x, ksize, stride=None, pad=0):
+	return TFAveragePooling2D(ksize, stride, pad, cover_all=False).apply((x,))[0]

+ 28 - 0
cvmodelz/utils/links/__init__.py

@@ -0,0 +1,28 @@
+import chainer
+import chainer.functions as F
+import chainer.links as L
+
+
+class Conv2D_BN(chainer.Chain):
+	def __init__(self, in_channels, out_channels, ksize,
+		stride=1, pad=0,
+		activation=F.relu,
+		nobias=True,
+		use_gamma=False,
+		eps=2e-5):
+
+		super(Conv2D_BN, self).__init__()
+		assert callable(activation)
+
+		with self.init_scope():
+			self.conv = L.Convolution2D(in_channels, out_channels,
+				ksize=ksize, stride=stride, pad=pad, nobias=nobias)
+			self.bn = L.BatchNormalization(out_channels,
+				use_gamma=use_gamma, eps=eps)
+
+		self.activation = activation
+
+	def forward(self, x):
+		x = self.conv(x)
+		x = self.bn(x)
+		return self.activation(x)

+ 40 - 0
cvmodelz/utils/links/pooling/__init__.py

@@ -0,0 +1,40 @@
+import chainer.functions as F
+
+from cvargparse.utils.enumerations import BaseChoiceType
+from functools import partial
+
+from cvmodelz.utils.functions import tf_average_pooling_2d
+from cvmodelz.utils.links.pooling.alpha_pooling import AlphaPooling
+from cvmodelz.utils.links.pooling.compact_bilinear import CompactBilinearPooling
+from cvmodelz.utils.links.pooling.global_average import GlobalAveragePooling
+
+def pool_creator(func):
+	def creator(ksize, stride=None, pad=0, **kwargs):
+		return partial(func, ksize=ksize, stride=stride, pad=pad)
+	return partial(creator)
+
+class PoolingType(BaseChoiceType):
+	MAX = pool_creator(F.max_pooling_2d)
+	AVG = pool_creator(F.average_pooling_2d)
+	TF_AVG = pool_creator(tf_average_pooling_2d)
+
+	G_AVG = GlobalAveragePooling
+	CBIL = CompactBilinearPooling
+	ALPHA = AlphaPooling
+
+	Default = G_AVG
+
+	@classmethod
+	def new(cls, pool_type, **kwargs):
+		pool_cls = cls.get(pool_type).value
+		return pool_cls(**kwargs)
+
+
+
+__all__ = [
+	"PoolingType",
+	"AlphaPooling",
+	"CompactBilinearPooling",
+	"GlobalAveragePooling",
+	"tf_average_pooling_2d",
+]

+ 36 - 0
cvmodelz/utils/links/pooling/alpha_pooling.py

@@ -0,0 +1,36 @@
+import chainer.functions as F
+
+from chainer import variable
+from chainer.initializers import Constant
+
+from cvmodelz.utils.links.pooling.compact_bilinear import CompactBilinearPooling
+from cvmodelz.utils.functions import alpha_prod
+
+class AlphaPooling(CompactBilinearPooling):
+	"""
+		AlphaPooling implementation based on CompactBilinearPooling and
+		Simon et al. (https://arxiv.org/abs/1705.00487)
+	"""
+	def __init__(self, init_alpha=1.0, eps=1e-5, use_built_ins=False, *args, **kwargs):
+		super(AlphaPooling, self).__init__(*args, **kwargs)
+
+		with self.init_scope():
+			self.alpha = variable.Parameter(
+				initializer=Constant(init_alpha), shape=(1,), name="alpha")
+			self.add_persistent("eps", eps)
+
+		self.use_built_ins = use_built_ins
+
+	def __call__(self, x):
+
+		if self.use_built_ins:
+			sgn_x = F.sign(x)
+			abs_x = F.absolute(x) + self.eps
+			big_a = F.broadcast_to(self.alpha - 1, x.shape)
+			x_hat = sgn_x * (abs_x ** big_a)
+		else:
+			x_hat = alpha_prod(x, self.alpha, eps=self.eps)
+
+		res = super(AlphaPooling, self).__call__(x_hat, x)
+		return res
+

+ 91 - 0
cvmodelz/utils/links/pooling/compact_bilinear.py

@@ -0,0 +1,91 @@
+from chainer import link
+from chainer import functions as F
+
+from cvmodelz.utils.functions import signed_square_root
+
+import numpy as np
+
+
+class CompactBilinearPooling(link.Link):
+	"""
+		Compact Bilinear Pooling based on
+		https://github.com/ronghanghu/tensorflow_compact_bilinear_pooling
+		and https://arxiv.org/abs/1511.06062
+	"""
+	def __init__(self, input_dim, output_dim, sum_pool=True, normalize=False, seed=2564643, **kw):
+		super(CompactBilinearPooling, self).__init__()
+
+		self.input_dim = input_dim
+		self.output_dim = output_dim
+
+		rnd = np.random.RandomState(seed)
+
+		h1 = rnd.randint(output_dim, size=input_dim).astype(np.int32)
+		s1 = rnd.choice([-1, 1], size=input_dim).astype(np.int32)
+
+		h2 = rnd.randint(output_dim, size=input_dim).astype(np.int32)
+		s2 = rnd.choice([-1, 1], size=input_dim).astype(np.int32)
+
+		m1 = self._sketch_matrix(h1, s1)
+		m2 = self._sketch_matrix(h2, s2)
+
+		self.add_persistent("sketch_matrix1", m1)
+		self.add_persistent("sketch_matrix2", m2)
+
+		self.add_persistent("sum_pool", sum_pool)
+		self.add_persistent("normalize", normalize)
+
+	def __call__(self, x1, x2=None):
+		if x2 is None:
+			x2 = x1
+
+		sketch1 = self._sketch(self.sketch_matrix1, x1)
+		sketch2 = self._sketch(self.sketch_matrix2, x2)
+
+		fft_s1, fft_s2 = self._fft(sketch1), self._fft(sketch2)
+
+		res = self._ifft(self._mul_imag(fft_s1, fft_s2))
+
+		if self.sum_pool:
+			res = F.mean(res, axis=(1,2))
+
+		if self.normalize:
+			res = signed_square_root(res)
+			# res = F.sign(res) * F.sqrt(F.absolute(res))
+			res = F.normalize(res)
+
+		return res
+
+	def _fft(self, var):
+		imag = self.xp.zeros_like(var.data)
+		return F.fft((var, imag))
+
+	def _ifft(self, var):
+		real, imag = F.ifft(var)
+		return real
+
+	def _mul_imag(self, xy, uv):
+		x, y = xy
+		u, v = uv
+		real = x*u - y*v
+		imag = x*v + y*u
+		return real, imag
+
+	def _sketch_matrix(self, h, s):
+		shape = (self.input_dim, self.output_dim)
+		res = self.xp.zeros(shape, dtype=np.float32)
+		ys = self.xp.arange(self.input_dim)
+		res[ys, h] = s
+		return res
+
+	def _sketch(self, mat, x):
+		x = F.transpose(x, axes=(0,2,3,1))
+		x_flat = F.reshape(x, (-1, x.shape[-1]))
+		res = F.tensordot(x_flat, mat, axes=1)
+		res_shape = x.shape[:-1] + (-1,)
+		return F.reshape(res, res_shape)
+
+
+
+
+

+ 15 - 0
cvmodelz/utils/links/pooling/global_average.py

@@ -0,0 +1,15 @@
+from chainer import link
+from chainer import functions as F
+
+class GlobalAveragePooling(link.Link):
+	output_dim=None
+
+	# just ignore all Arguments
+	def __init__(self, **kw):
+		super(GlobalAveragePooling, self).__init__()
+
+	def forward(self, x):
+		n, channel, rows, cols = x.shape
+		h = F.average_pooling_2d(x, (rows, cols), stride=1)
+		h = F.reshape(h, (n, channel))
+		return h

+ 1 - 1
tests/model_tests/creation.py

@@ -3,8 +3,8 @@ import unittest
 
 from cvmodelz.models import ModelFactory
 from cvmodelz.models.pretrained.base import PretrainedModelMixin
-from chainer_addons.links.pooling import GlobalAveragePooling # TODO: replace this!
 from cvmodelz.models.wrapper import ModelWrapper
+from cvmodelz.utils.links.pooling import GlobalAveragePooling
 
 
 class ModelCreationsTests(unittest.TestCase):