* @param x the inputs.
	 * @param y the targets.
	 * @param maxNumIters the maximum number of iterations.
	 * @param numLambdas the number of lambdas.
	 * @param minLambdaRatio the minimum lambda is minLambdaRatio * max lambda.
	 * @return group-lasso penalized classifiers.
	 */
	public List<GLM> buildBinaryClassifiers(int[][] attrs, double[][][] x, double[] y, int maxNumIters, int numLambdas, double minLambdaRatio) {
		int p = 0;
		if (attrs.length > 0) {
			for (int[] attr : attrs) {
				p = Math.max(p, StatUtils.max(attr));
			}
			p += 1;
		}
		
		double[][] w = new double[attrs.length][];
		int m = 0;
		for (int j = 0; j < attrs.length; j++) {
			w[j] = new double[attrs[j].length];
			if (w[j].length > m) {
				m = w[j].length;
			}
		}

		double[] pTrain = new double[y.length];
		double[] rTrain = new double[y.length];
		OptimUtils.computePseudoResidual(pTrain, y, rTrain);

		double[] g = new double[m];
		double[] gradient = new double[m];

		double[] tl1 = new double[x.length];

		double[] stepSize = new double[x.length];
		for (int j = 0; j < stepSize.length; j++) {
			double max = 0;
			double[][] block = x[j];
			for (double[] t : block) {
				double l = StatUtils.sumSq(t) / 4;
				if (l > max) {
					max = l;
				}
			}
			stepSize[j] = 1.0 / max;
		}

		boolean[] activeSet = new boolean[x.length];

		double maxLambda = findMaxLambdaBinomial(x, y, pTrain, rTrain, gradient);

		// Dampening factor for lambda
		double alpha = Math.pow(minLambdaRatio, 1.0 / numLambdas);

		// Compute the regularization path
		List<GLM> glms = new ArrayList<>(numLambdas);
		double lambda = maxLambda;
		double intercept = 0;
		Set<ModelStructure> structures = new HashSet<>();
		for (int l = 0; l < numLambdas; l++) {
			// Initialize regularization parameters
			for (int j = 0; j < tl1.length; j++) {
				tl1[j] = lambda * Math.sqrt(w[j].length);
			}

			// Block coordinate gradient descent
			int iter = 0;
			while (iter < maxNumIters) {

				if (fitIntercept) {
					intercept += OptimUtils.fitIntercept(pTrain, rTrain, y);
				}

				boolean activeSetChanged = doOnePassBinomial(x, y, tl1, true, activeSet, w,
						stepSize, g, gradient, pTrain, rTrain);

				iter++;

				if (!activeSetChanged || iter > maxNumIters) {
					break;
				}

				for (; iter < maxNumIters; iter++) {

					double prevLoss = GLMOptimUtils.computeGroupLassoLoss(pTrain, y, w, tl1);
					
					if (fitIntercept) {
						intercept += OptimUtils.fitIntercept(pTrain, rTrain, y);
					}
					
					doOnePassBinomial(x, y, tl1, false, activeSet, w, stepSize, g, gradient, pTrain, rTrain);

					double currLoss = GLMOptimUtils.computeGroupLassoLoss(pTrain, y, w, tl1);

					if (OptimUtils.isConverged(prevLoss, currLoss, epsilon)) {
						break;
					}

					if (verbose) {
						System.out.println("Iteration " + iter + ": " + currLoss);
					}
				}
			}

			lambda *= alpha;
			if (refit) {
				boolean[] selected = new boolean[attrs.length];
				for (int i = 0; i < selected.length; i++) {
					selected[i] = !ArrayUtils.isConstant(w[i], 0, w[i].length, 0);
				}
				ModelStructure structure = new ModelStructure(selected);
				if (!structures.contains(structure)) {
					GLM glm = refitClassifier(p, attrs, selected, x, y, w, maxNumIters);
					glms.add(glm);
					structures.add(structure);
				}
			} else {
				GLM glm = getGLM(p, attrs, w, intercept, LinkFunction.LOGIT);
				glms.add(glm);
			}
		}

		return glms;
	}