This version of LiblineaR updates its bundled copy of
the LIBLINEAR C/C++ library from v2.1 (2015) to v2.50 (2024) – the first
upgrade of the bundled library in a decade, since LiblineaR
2.10-0 in July 2016. This vignette covers two things: the new
capabilities this brings at the R level, and evidence that the upgrade
is safe to rely on, along with what it changes in practice.
Three new capabilities are available, on top of everything
LiblineaR already supported.
type=21 fits a one-class SVM. Unlike every other
type, it is unsupervised – it learns what
“normal” data looks like from data alone.
target is still required as an argument (its length must
match the number of rows of data) but its values
are ignored. bias must be <= 0 (this solver
has no bias-term concept), and nu (a number in (0, 1),
default 0.5) is an upper bound on the fraction of training points that
may be treated as outliers.
predict() returns 1 for inliers and
-1 for outliers; decisionValues=TRUE
additionally returns the raw signed distance to the decision boundary
(positive means inlier – useful if you want to rank points by how
anomalous they are, rather than just a yes/no call).
x <- iris[, 1:4]
y <- iris[, 5]
m_oc <- LiblineaR(x, y, type = 21, bias = -1, nu = 0.1)
p_oc <- predict(m_oc, newx = x, decisionValues = TRUE)
table(p_oc$predictions)
#>
#> 1
#> 150
head(p_oc$decisionValues)
#> [,1]
#> [1,] 543.29
#> [2,] 506.37
#> [3,] 499.75
#> [4,] 492.30
#> [5,] 541.00
#> [6,] 589.24regularizeBias: excluding the intercept from
regularizationBy default, LiblineaR’s bias term (the intercept) is
penalized by the L1/L2 regularization exactly like every other
coefficient. Setting regularizeBias=FALSE excludes it, so
it is fit freely. This is only valid combined with bias=1
exactly, and only for type 0, 2, 5, 6 or 11; other
combinations raise a clear error. Most users should leave this at its
default (TRUE) – an unregularized intercept is a
specialized choice, useful mainly when you have strong reasons to
believe the regularization penalty should apply only to the feature
weights, not to the overall offset.
findC now supports SVR (type=11)For perspective: heuristicC(data) and
findC=TRUE both help pick a good cost, but
they work very differently. heuristicC() is a fast,
closed-form formula – it inspects data once and returns a
single suggested cost, with no training involved at all.
findC=TRUE is an actual empirical search – it repeatedly
trains and cross-validates across a doubling sequence of
cost values and returns whichever one empirically scored
best. heuristicC() is a cheap starting guess;
findC is expensive but data-driven tuning, and the two
compose naturally: useInitC=TRUE (the default) uses the
cost you pass in – often heuristicC()’s own
suggestion – as findC’s starting point for the search.
findC=TRUE previously only searched for a good
cost with type=0 and type=2. It
now also supports type=11 (L2-regularized L2-loss SVR),
additionally searching svr_eps at the same time. For this
type only, findC returns a named list
list(C=..., svr_eps=...) instead of a plain number, since
there are now two values to report; type 0 and 2 are
unaffected and still return a plain number.
xr <- as.matrix(iris[, 1:3])
yr <- iris[, 4]
best <- LiblineaR(xr, yr, type = 11, findC = TRUE, cross = 5)
#> Warning in LiblineaR(xr, yr, type = 11, findC = TRUE, cross = 5): No value
#> provided for svr_eps. Using default of 0.1
best
#> $C
#> [1] 1
#>
#> $svr_eps
#> [1] 0
m_svr <- LiblineaR(xr, yr, type = 11, cost = best$C, svr_eps = best$svr_eps)Beyond LiblineaR’s own regression test suite, this
section reports an independent check: models were trained with the
pristine, unmodified LIBLINEAR command-line tools
(train/predict, compiled directly from
upstream source, no R or LiblineaR code involved at all)
and compared against LiblineaR’s own output, at both the
old (v2.10) and new (v2.50) LIBLINEAR version.
Four engines were compared: the LIBLINEAR command-line tools at v2.10
and v2.50, and the LiblineaR R package before and after
this upgrade. All four were trained on byte-identical
data (the same train/test split of the same matrix) with
matching hyperparameters (cost, bias,
epsilon, svr_eps). Four datasets were used:
two synthetic classification problems (2-class and 3-class, 600 rows
each), one regression problem (600 rows), and one larger problem (8000
rows, 300 columns) reserved for timing.
A methodological caveat, confirmed empirically before
trusting any of the numbers below: LIBLINEAR’s dual
coordinate-descent solvers (and the L1-regularized solvers) shuffle
their working set using a random number generator.
LiblineaR routes this through R’s own RNG
(set.seed()-controlled), while the plain command-line tools
use C’s own unseeded rand(). For these solver
types, coefficients are not expected to match between
LiblineaR and the command-line tools, even at the identical
LIBLINEAR version – they are two different, independent,
equally valid solutions to the same problem. What is expected,
and checked below, is that predictions agree almost exactly. Primal,
non-randomized solvers (logistic regression and L2-loss SVM/SVR,
type 0, 2, 11) use no randomness at all, so their
coefficients are expected to match the command-line tools almost to
floating-point precision – and are the real fidelity check.
LiblineaR still correctly wrap
LIBLINEAR?Maximum absolute coefficient difference between
LiblineaR and the pristine command-line tools, same
LIBLINEAR version, across the 3 correctness datasets (28 train/predict
comparisons per version):
fidelity <- data.frame(
type = c(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 11L, 12L, 13L),
solver = c("L2R_LR (primal)", "L2R_L2LOSS_SVC_DUAL", "L2R_L2LOSS_SVC (primal)",
"L2R_L1LOSS_SVC_DUAL", "MCSVM_CS", "L1R_L2LOSS_SVC", "L1R_LR",
"L2R_LR_DUAL", "L2R_L2LOSS_SVR (primal)", "L2R_L2LOSS_SVR_DUAL",
"L2R_L1LOSS_SVR_DUAL"),
max_abs_diff_v210 = c(1.72e-10, 9.65e-03, 1.98e-10, 3.60e-02, 1.38e-02,
3.93e-01, 6.82e-01, 7.32e-03, 1.84e-10, 7.93e-02, 1.71e-01),
max_abs_diff_v250 = c(1.75e-10, 9.65e-03, 1.93e-10, 3.60e-02, 1.38e-02,
3.93e-01, 6.82e-01, 7.32e-03, 1.59e-10, 7.93e-02, 1.71e-01)
)
knitr::kable(fidelity, digits = 4,
col.names = c("type", "solver", "max|diff| vs CLI (2.10)", "max|diff| vs CLI (2.50)"))| type | solver | max|diff| vs CLI (2.10) | max|diff| vs CLI (2.50) |
|---|---|---|---|
| 0 | L2R_LR (primal) | 0.0000 | 0.0000 |
| 1 | L2R_L2LOSS_SVC_DUAL | 0.0097 | 0.0097 |
| 2 | L2R_L2LOSS_SVC (primal) | 0.0000 | 0.0000 |
| 3 | L2R_L1LOSS_SVC_DUAL | 0.0360 | 0.0360 |
| 4 | MCSVM_CS | 0.0138 | 0.0138 |
| 5 | L1R_L2LOSS_SVC | 0.3930 | 0.3930 |
| 6 | L1R_LR | 0.6820 | 0.6820 |
| 7 | L2R_LR_DUAL | 0.0073 | 0.0073 |
| 11 | L2R_L2LOSS_SVR (primal) | 0.0000 | 0.0000 |
| 12 | L2R_L2LOSS_SVR_DUAL | 0.0793 | 0.0793 |
| 13 | L2R_L1LOSS_SVR_DUAL | 0.1710 | 0.1710 |
Primal types (0, 2, 11) match the pristine command-line tools to
1e-10, at both versions – direct evidence that
LiblineaR correctly passes data through to the same
underlying solver, unaffected by the upgrade. Every dual/L1 type shows a
larger difference, exactly as the caveat above predicts – and across all
54 such comparisons (both versions, all 3 datasets), minimum
prediction agreement was 98.75%, with most exactly 100%,
confirming these are two valid solutions to the same problem, not a
correctness gap.
Maximum absolute coefficient difference between
LiblineaR before and after the upgrade, same 3
datasets:
upgrade <- data.frame(
type = c(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 11L, 12L, 13L),
solver = fidelity$solver,
max_abs_diff = c(0.0789, 0, 0.2281, 0, 0, 0, 0, 0, 0.0021, 0, 0)
)
knitr::kable(upgrade, digits = 4, col.names = c("type", "solver", "max|diff| (2.10-26 vs. 2.50-1)"))| type | solver | max|diff| (2.10-26 vs. 2.50-1) |
|---|---|---|
| 0 | L2R_LR (primal) | 0.0789 |
| 1 | L2R_L2LOSS_SVC_DUAL | 0.0000 |
| 2 | L2R_L2LOSS_SVC (primal) | 0.2281 |
| 3 | L2R_L1LOSS_SVC_DUAL | 0.0000 |
| 4 | MCSVM_CS | 0.0000 |
| 5 | L1R_L2LOSS_SVC | 0.0000 |
| 6 | L1R_LR | 0.0000 |
| 7 | L2R_LR_DUAL | 0.0000 |
| 11 | L2R_L2LOSS_SVR (primal) | 0.0021 |
| 12 | L2R_L2LOSS_SVR_DUAL | 0.0000 |
| 13 | L2R_L1LOSS_SVR_DUAL | 0.0000 |
Only the 3 primal Newton-solved types (0, 2, 11) show any drift at all: LIBLINEAR replaced its trust-region Newton solver with a faster-converging line-search variant partway through this version range, refined further by later releases. Every dual/L1 type was bit-for-bit identical on these ordinary, well-conditioned datasets – LIBLINEAR 2.50’s lower iteration cap on dual coordinate descent (see Result 3) was never actually reached on data this well-behaved. Predictive performance (accuracy for classification, mean squared error for regression) matched to 4 decimal places for every type on every dataset.
LIBLINEAR 2.50 caps its dual coordinate-descent solvers at 300 iterations (down from 1000 in earlier versions) and, for three solver types, automatically switches to the primal solver instead of running out the clock if that cap is reached. Result 2 shows this is invisible on easy-to-fit data. It stops being invisible on a harder problem. On a larger, more weakly-separated dataset (8000 rows, 300 columns):
large <- data.frame(
type = c(0L, 1L, 7L, 11L, 12L),
solver = c("L2R_LR (primal)", "L2R_L2LOSS_SVC_DUAL", "L2R_LR_DUAL",
"L2R_L2LOSS_SVR (primal)", "L2R_L2LOSS_SVR_DUAL"),
metric_old = c(0.9188, 0.9219, 0.9200, 1.0234, 1.0502),
metric_new = c(0.9181, 0.9188, 0.9200, 1.0235, 1.0234),
time_old_s = c(0.133, 0.632, 1.035, 0.075, 3.400),
time_new_s = c(0.147, 0.288, 0.998, 0.086, 1.201),
speedup = c(0.90, 2.19, 1.04, 0.87, 2.83)
)
knitr::kable(large, digits = 4,
col.names = c("type", "solver", "accuracy/MSE (2.10-26)", "accuracy/MSE (2.50-1)",
"time 2.10-26 (s)", "time 2.50-1 (s)", "speedup"))| type | solver | accuracy/MSE (2.10-26) | accuracy/MSE (2.50-1) | time 2.10-26 (s) | time 2.50-1 (s) | speedup |
|---|---|---|---|---|---|---|
| 0 | L2R_LR (primal) | 0.9188 | 0.9181 | 0.133 | 0.147 | 0.90 |
| 1 | L2R_L2LOSS_SVC_DUAL | 0.9219 | 0.9188 | 0.632 | 0.288 | 2.19 |
| 7 | L2R_LR_DUAL | 0.9200 | 0.9200 | 1.035 | 0.998 | 1.04 |
| 11 | L2R_L2LOSS_SVR (primal) | 1.0234 | 1.0235 | 0.075 | 0.086 | 0.87 |
| 12 | L2R_L2LOSS_SVR_DUAL | 1.0502 | 1.0234 | 3.400 | 1.201 | 2.83 |
Two solver types where the iteration cap is actually reached on this
dataset (type 1 and type 12, both dual) show a
2.2x and 2.8x speedup respectively – and
type 12’s test-set mean squared error also
improved (1.050 → 1.023), because the previous solver spent its
full 1000-iteration budget grinding toward a barely-better optimum on a
genuinely slow-converging problem, while the current one recognizes the
cap, switches solver, and reaches a comparably good (here, slightly
better) answer in a fraction of the time. The primal Newton types (0,
11) are essentially a wash in either direction (0.87-0.90x) – expected,
since that solver’s algorithm changed but not its overall cost.
findC
support for SVR.LiblineaR continues to
faithfully wrap LIBLINEAR, confirmed against the pristine command-line
tools directly, not just against its own prior output. Coefficient-level
agreement for every non-randomized solver is at floating-point
precision.Reproducibility note. These figures were measured on
R version 4.6.1 (2026-06-24), Linux, using LIBLINEAR upstream source at
tags v210 and v250
(github.com/cjlin1/liblinear), compiled standalone with no changes.
set.seed() was fixed identically across
LiblineaR calls for a given comparison; the command-line
tools have no equivalent seed control (see the caveat above). Datasets
are synthetic and fixed by seed; absolute numbers will differ on other
data, but the qualitative pattern (primal solvers: no timing benefit,
but a different algorithm; dual solvers: identical on easy data, faster
and possibly more accurate on hard data) is expected to hold generally,
since it follows directly from the documented behavior of the underlying
LIBLINEAR change, not from anything data-specific.
Authorship note. The scope and content of this report – what to test, which comparisons matter, and how to interpret the results – were designed and directed by the package maintainer. Drafting of this vignette and the accompanying comparison scripts was AI-assisted (Claude, Anthropic). Every figure was produced by actually running the real LIBLINEAR and LiblineaR software, not generated by the AI; all code, results, and conclusions were reviewed and validated by the maintainer before publication.