2. 稀疏约束优化的变体#
除了标准的稀疏约束优化(SCO)问题外,skscope 还提供了对几种有用的SCO变体的支持。
2.1. 分组结构参数#
在某些情况下,我们可能会遇到分组结构的参数,其中所有参数被划分为不重叠的组。这种情况的例子包括线性模型下的组变量选择[1]、多任务学习等。
在处理具有组结构的参数时,我们将每个参数组视为一个单元,同时选择或取消选择组中的所有参数。这个问题被称为组SCO(GSCO)。
例如,如果我们的目标是从总共\(q\)个参数组中选择\(s\)组参数,GSCO问题可以表述如下:
其中 \(G=\{g_1, \dots, g_q\}\) 是满足以下条件的索引集的分区:
\(g_i \subseteq \{1, \dots, p\}\) 对于 \(i=1, \dots, q\),
\(g_i \cap g_j = \emptyset\) 对于 \(i \neq j\),
和
\(\bigcup_{i=1}^q g_i = \{1, \dots, p\}\).
特别是,如果 \(q=p\) 且 \(g_i = \{i\}\)(对于 \(i=1, \dots, q\)),那么 GSCO 等同于 SCO。因此,GSCO 是 SCO 的推广。
当遇到GSCO时,我们使用skscope提供的求解器中的group参数。group参数是一个从0开始无间隙的递增整数数组,长度为维度数。这意味着同一组内的变量必须是相邻的,并且它们将一起被选择或取消选择。以下是一些无效的group参数数组示例:
group = [0, 2, 1, 2](非递增),group = [1, 2, 3, 3](不从0开始),group = [0, 2, 2, 3](包含一个间隔).
在使用skscope解决GSCO时,请注意
the
dimensionality参数是所有参数的数量,sparsity参数表示 要选择的参数组数量。
from skscope import ScopeSolver
q, s, m = 3, 2, 2
params_true = [1, 10, 0, 0, -1, 5]
ScopeSolver(
dimensionality=q * m, ## the total number of parameters
sparsity=s, ## the number of parameter groups to be selected
group=[0, 0, 1, 1, 2, 2], ## specify group structure
)
2.2. 预选非稀疏参数#
在使用各种模型时,通常会有一些参数必须具有非零值。例如:
在具有截距的线性模型中,截距项通常被假定为非稀疏的;
在Ising模型中,对应矩阵的对角线项表示外部磁场的强度,通常被认为是非零的。
让 \(\mathcal{P}\) 表示预选参数的集合,广义SCO被表述为:
skscope 允许用户使用 preselect 参数来指定这些预选的非稀疏参数。该参数是一个整数列表,求解器将始终选择这些参数。
以下是preselect使用的一个示例:
from skscope import ScopeSolver
solver = ScopeSolver(
dimensionality=10, ## 10 parameters in total
sparsity=3, ## 3 non-sparse parameters to be selected
preselect=[0, 1], ## always select the first two parameters as non-sparse values
)
2.3. 层#
Layer,即skscope.layer中定义的任何类,是目标函数的“装饰器”。
参数在进入目标函数之前将由Layer处理。
不同的层可以实现不同的效果,
并且它们可以顺序连接在一起形成一个更大的层,
从而实现更复杂的功能。
在实践中,可能有一些参数的要求可以通过修改目标函数来实现。 例如,如果我们希望未选择特征对应的参数等于一个常数而不是零,即
其中 \(\mu \in R^p\) 是一个偏移向量。为此,我们可以将原始问题重新表述如下:
其中 \(L(\theta') = \theta' + \mu\) 是一个向参数添加常数偏移的层。
在skscope中,我们可以通过稀疏求解器的solve方法中的layers参数来实现这一点。
from skscope import ScopeSolver
import skscope.layer as Layer
from jax import numpy as jnp
X = jnp.array([[1, 2, 3], [4, 5, 6]])
y = jnp.array([1, 2])
def loss(params):
return jnp.sum((X @ params - y) ** 2)
solver = ScopeSolver(3, 1)
solver.solve(
loss,
layers=[Layer.OffsetSparse(dimensionality=3, offset=1)],
)
print(solver.get_estimated_params())
params 在进入 loss 之前将通过一个偏移层 OffsetSparse。
这样,未选择特征对应的参数将等于1而不是零。
此外,我们可以同时使用多个层来实现更复杂的功能。
layers 是一个层的列表,参数在进入 loss 之前会按顺序通过这些层。
solver.solve(
loss,
layers=[
Layer.LinearConstraint(dimensionality=3, coef=jnp.array([[1, 1, 1]])),
Layer.NonNegative(dimensionality=3),]
)
这将为参数添加一个线性约束 \(\theta_1 + \theta_2 + \theta_3 = 1\) 和一个非负约束。
在skscope.layer中,我们提供了几种用于重新参数化的层:NonNegative、LinearConstraint、SimplexConstraint和BoxConstraint。
此外,用户还可以通过继承skscope.layer.Identity类来定义自己的层。
2.4. 灵活的优化接口#
对于skscope中的所有求解器(除了IHTSolver),这些求解器中不可或缺的一步是解决一个优化问题:
其中
\(\theta\) 是一个 \(s\) 维参数向量(注意 \(s\) 是 SCO 中所需的稀疏性)
\(f(\theta)\) 是目标函数;
所有在skscope中的求解器都使用scipy.optimize中的L-BFGS-B算法作为此问题的默认数值优化求解器。
在某些情况下,\(\theta\)的内在结构可能会有额外的约束,这些约束可以表述为一个集合\(\mathcal{C}\):
一个典型的例子是用于连续随机变量的高斯图模型,它将\(\theta\)限制在对称正定空间上(参见此示例gaussian precision matrix)。尽管默认的数值求解器无法解决这个问题,skscope提供了一个灵活的接口,允许替换它。具体来说,用户可以通过正确设置求解器中的numeric_solver来更改默认的数值优化求解器。
> 请注意,
numeric_solver的接受输入应与skscope.numeric_solver.convex_solver_LBFGS具有相同的接口。
from skscope import ScopeSolver
def custom_numeric_solver(*args, **kwargs):
params = []
# do something about params
return params
p, k = 10, 3
solver = ScopeSolver(p, k, numeric_solver=custom_numeric_solver)
此功能显著扩展了skscope的应用范围,使其能够与Python中的其他强大优化工具包合作。
我们将简要介绍一些示例:
2.5. 参考#
[1] 张, Y., 朱, J., 朱, J., & 王, X. (2023). 一种拼接方法用于最佳子集组选择. INFORMS 计算杂志, 35(1), 104-119.