The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
A neural network can learn to solve the cube without ever being shown a solution. It learns the distance to solved: how many moves a state needs. A solver then picks, at every step, the move that brings the cube closest to solved.
The networks need the ggmlR package. They run on the CPU, or on a GPU through Vulkan when one is available. Training takes from minutes to hours, so most of the code below is shown but not run.
generate_walk()Every example starts with random walks away from the solved cube.
generate_walk() returns each walk together with the moves
it took, so the way back is known in advance. That makes it useful both
for training and for checking a solver against a known answer.
g <- cube_group(3)
set.seed(1)
w <- generate_walk(g, n = 3, n_moves = 6)
w$moves[[1]]
#> [1] "F" "D" "B" "S" "R'" "S"
w$solution[[1]]
#> [1] "S'" "R" "S'" "B'" "D'" "F'"
# the solution really does solve the state
identical(group_apply(g, w$states[1, ], w$solution[[1]]), group_identity(g))
#> [1] TRUEA walk’s length is only an upper bound on the true distance, because
a shorter way back may exist. Two options make the bound tighter:
no_undo (on by default) refuses a move that undoes the one
before, and no_cancel also refuses moves like
U D U', which walk three moves and travel one.
For training, exact = FALSE draws walk lengths
uniformly, so every depth is equally represented:
The obvious label for a scrambled state is the length of the scramble. That label is wrong, because a scramble of 15 moves may be solvable in 12. Autodidactic iteration (ADI, from DeepCubeA) avoids the problem. The target of a state is
1 + the smallest value the network gives to any of its children
and a solved child counts as exactly 0. States one move from solved get exact targets right away, their neighbours learn from them, and correctness spreads outward as training goes on.
cube_adi_model() builds two networks over the same
input. The value network estimates the distance; the
policy network estimates which move is best.
The main choices are:
arch: "mlp" (the default),
"resnet" (the shape DeepCubeA uses),
"transformer" (needs ggmlR 0.8.5 or newer), and
"lstm" / "gru" for comparison.encoding: "sticker" reads the 54 stickers;
"piece" reads which piece sits in each slot and how it is
turned, as DeepCubeA does.backend: "auto", "cpu" or
"vulkan".net <- cube_adi_train(net, iterations = 200, batch_states = 10000,
max_depth = 20)
tail(net$history)Each iteration draws fresh scrambles, computes their targets with a
frozen copy of the value network, and fits both networks. The copy is
refreshed when the loss falls below loss_thresh, or every
refresh_every iterations if you set that.
In the printed log, watch the mean target: it should
rise as correctness spreads. If it stalls near 1, the frozen copy is not
being refreshed. Raise loss_thresh or set
refresh_every.
A trained network is saved to a directory and loaded back with
everything it needs, including its group. A loaded network can be
trained further: cube_adi_train() simply continues from its
weights.
There are two ways to use a trained network.
Greedy: cube_adi_solve() takes the
best-looking move at every step. It never revisits a state, but it never
reconsiders a choice either. It is fast, and works well near solved,
where the network is accurate.
A* search: cube_adi_astar() keeps the
moves it passed over and returns to them when the chosen branch stops
paying. A wrong estimate then costs time rather than the solve.
set.seed(7)
s <- generate_walk(g, n = 1, n_moves = 15)$states[1, ]
greedy <- cube_adi_solve(net, s, budget = 200)
greedy$solved
length(greedy$path)
astar <- cube_adi_astar(net, s, weight = 0.6, batch = 1000)
astar$solved
astar$length
astar$nodes # states expandedA* orders states by
weight * moves_so_far + estimated_distance.
weight = 1 is classic A*: shorter paths, much
slower.weight = 0 is pure greedy best-first.batch expands that many states per step and scores all
their children in one call. This keeps a GPU busy, but a very large
batch can make solutions longer.
Because generate_walk() knows the way back, it is easy
to see how a solver behaves as the scramble gets deeper:
The package also ships scripts for a different kind of network, a
Q-model. A Q-model scores all 18 moves of a state at
once instead of one distance per state. It is trained on
generate_walk(pivot = TRUE): a point in the middle of a
walk, plus the two moves whose value is known there, the move back and
the move forward.
inst/examples/train_cube3_q_online.R trains a
transformer Q-model on fresh walks at every step, so no state is seen
twice. It saves checkpoints with ckpt=, continues an
interrupted run with resume=, and trains a saved model
further with init=:
Rscript inst/examples/train_cube3_q_online.R steps=20000 ckpt=1000
Rscript inst/examples/train_cube3_q_online.R init=<model dir> depth=16 steps=8000 lr=5e-4inst/examples/diag_cube3_q.R takes a model and
solves scrambles of falling depth, greedily and by beam search side by
side. That shows at which depth the model starts to fail.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.