This commit is contained in:
milner committed 2026-09-18 16:27:30 +00:00
commit 3170b7b398
96 files changed
+39927

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
(executable
(name main)
(flags (:standard -warn-error -a))
(libraries scfg))
+289
View File
@@ -0,0 +1,289 @@
open Scfg
let read_file path =
let ic = open_in path in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
let mkdir_p dir = ignore (Sys.command ("mkdir -p " ^ Filename.quote dir))
let parse_args rest =
let rec go opts = function
| [] -> opts
| x :: v :: tl
when String.length x > 2
&& String.sub x 0 2 = "--"
&& String.length v > 0
&& v.[0] <> '-' ->
go ((String.sub x 2 (String.length x - 2), v) :: opts) tl
| x :: tl when String.length x > 2 && String.sub x 0 2 = "--" ->
go ((String.sub x 2 (String.length x - 2), "true") :: opts) tl
| _ :: tl -> go opts tl
in
go [] rest
let opt opts k d = match List.assoc_opt k opts with Some v -> v | None -> d
let float_opt opts k d =
match List.assoc_opt k opts with
| Some v -> (match float_of_string_opt v with Some f -> f | None -> d)
| None -> d
let int_opt opts k d =
match List.assoc_opt k opts with
| Some v -> (match int_of_string_opt v with Some i -> i | None -> d)
| None -> d
let flag opts k = List.mem_assoc k opts
let config_of_opts opts =
{
Scoring.default_config with
alpha = float_opt opts "alpha" Scoring.default_config.alpha;
prior_weight = float_opt opts "prior" Scoring.default_config.prior_weight;
em_iters = int_opt opts "em-iters" Scoring.default_config.em_iters;
max_chunk = int_opt opts "max-chunk" Scoring.default_config.max_chunk;
mode =
(match opt opts "mode" "marginal" with
| "ml" -> Scoring.Maximum_likelihood
| "variational" -> Scoring.Variational
| _ -> Scoring.Marginal);
chunk_occurrence =
(if opt opts "chunk-occurrence" "all" = "first" then
Scoring.First_occurrence
else Scoring.All_occurrences);
}
let usage () =
print_endline
"SCFG induction by Bayesian model merging (Stolcke & Omohundro 1994)\n\n\
Usage: scfg-induction <command> [options]\n\n\
Commands:\n\
\ induce --corpus FILE [--out DIR] [--beam N] [--max-steps N]\n\
\ [--patience N] [--alpha A] [--prior W] [--max-chunk N]\n\
\ [--em-iters N] [--mode marginal|ml|variational]\n\
\ [--chunk-occurrence all|first] [--render]\n\
\ [--search beam|bestfirst] [--frontier N] [--max-expansions N]\n\
\ score --grammar FILE --corpus FILE [--alpha A] [--prior W]\n\
\ parse --grammar FILE --input \"a b c\" [--dot FILE]\n\
\ dot --grammar FILE --out FILE [--svg FILE] [--title T]\n\
\ experiments [--out DIR] [--seed N] [--beam N] [--max-steps N]\n\
\ [--patience N] [--max-len N] [--only NAME] [--quick]\n\n\
Corpus format: one sequence per line, whitespace-separated tokens.\n\
An optional \"count:\" prefix sets the sample count, e.g. \"3: a b c\".\n\
Grammar format: \"S -> A B [0.5]\"; uppercase-initial symbols are\n\
nonterminals, other symbols are terminals."
let cmd_induce opts =
let corpus_path = opt opts "corpus" "" in
if corpus_path = "" then (prerr_endline "induce: --corpus is required"; exit 2);
let out_dir = opt opts "out" "out" in
mkdir_p out_dir;
let steps_dir = Filename.concat out_dir "steps" in
mkdir_p steps_dir;
let render = flag opts "render" in
let config = config_of_opts opts in
let beam = int_opt opts "beam" 4 in
let max_steps = int_opt opts "max-steps" 25 in
let patience = int_opt opts "patience" 5 in
let corpus = Corpus.of_file corpus_path in
let initial =
Grammar.initial_grammar
(List.map (fun s -> (s.Corpus.tokens, s.Corpus.count)) corpus)
in
Printf.printf "corpus: %d distinct samples, total count %g\n"
(List.length corpus) (Corpus.total_count corpus);
Printf.printf "initial grammar: %d nonterminals, %d productions\n"
(List.length (Grammar.nonterminals initial))
(List.length initial.Grammar.productions);
let result =
if opt opts "search" "beam" = "bestfirst" then
Search.run_best_first ~config
~frontier_cap:(int_opt opts "frontier" 200)
~max_expansions:(int_opt opts "max-expansions" 300)
~patience initial corpus
else Search.run ~config ~beam_width:beam ~max_steps ~patience initial corpus
in
List.iteri
(fun i (s : Search.step) ->
let ch = Transform.change_of s.Search.parent s.Search.transform s.Search.result in
let base =
Filename.concat steps_dir (Printf.sprintf "step%03d" (i + 1))
in
let dot_before =
Dot.dot_grammar ~highlight_nts:ch.Transform.before_nts
~highlight_prods:ch.Transform.before_prods
~title:
(Printf.sprintf "step %d before: %s" (i + 1)
(Transform.describe s.Search.transform))
s.Search.parent
in
let dot_after =
Dot.dot_grammar ~highlight_nts:ch.Transform.after_nts
~highlight_prods:ch.Transform.after_prods
~title:
(Printf.sprintf "step %d after: %s" (i + 1)
(Transform.describe s.Search.transform))
s.Search.result
in
let svg_before = if render then Some (base ^ "-before.svg") else None in
let svg_after = if render then Some (base ^ "-after.svg") else None in
ignore
(Dot.render ?out_svg:svg_before ~out_dot:(base ^ "-before.dot") dot_before);
ignore
(Dot.render ?out_svg:svg_after ~out_dot:(base ^ "-after.dot") dot_after);
Printf.printf " accepted step %d: %s score %.4f -> %.4f\n" (i + 1)
(Transform.describe s.Search.transform)
s.Search.parent_score s.Search.score)
result.Search.steps;
let details = Scoring.details ~config result.Search.best corpus in
Dot.write_file (Filename.concat out_dir "grammar-final.dot")
(Dot.dot_grammar ~title:"induced grammar" result.Search.best);
if render then
ignore
(Dot.render ~out_svg:(Filename.concat out_dir "grammar-final.svg")
~out_dot:(Filename.concat out_dir "grammar-final.dot")
(Dot.dot_grammar ~title:"induced grammar" result.Search.best));
Dot.write_file (Filename.concat out_dir "grammar-initial.dot")
(Dot.dot_grammar ~title:"initial grammar" initial);
Dot.write_file (Filename.concat out_dir "grammar-final.txt")
(Grammar.to_string result.Search.best);
Dot.write_file (Filename.concat out_dir "search-steps.tsv")
(Search.steps_to_tsv result);
Dot.write_file (Filename.concat out_dir "search-proposals.tsv")
(Search.proposals_to_tsv result);
Printf.printf "\nsearch: %d iterations, %d accepted steps\n"
result.Search.iterations (List.length result.Search.steps);
Printf.printf "posterior: %.4f -> %.4f\n" result.Search.initial_score
result.Search.best_score;
Printf.printf "structure prior %.4f, marginal loglik %.4f, dl %.1f bits\n"
details.Scoring.prior details.Scoring.marginal details.Scoring.dl_bits;
Printf.printf "final grammar: %d nonterminals, %d productions\n"
details.Scoring.num_nonterminals details.Scoring.num_productions;
print_endline "\nfinal grammar:";
print_endline (Grammar.to_string result.Search.best);
Printf.printf "\nwrote %s\n" out_dir
let cmd_score opts =
let grammar_path = opt opts "grammar" "" in
let corpus_path = opt opts "corpus" "" in
if grammar_path = "" || corpus_path = "" then
(prerr_endline "score: --grammar and --corpus are required"; exit 2);
let config = config_of_opts opts in
let g = Grammar.of_string (read_file grammar_path) in
let corpus = Corpus.of_file corpus_path in
let d = Scoring.details ~config g corpus in
Printf.printf "posterior %.6f\n" d.Scoring.posterior;
Printf.printf "structural prior %.6f\n" d.Scoring.prior;
Printf.printf "marginal loglik %.6f\n" d.Scoring.marginal;
Printf.printf "ML corpus loglik %.6f\n" d.Scoring.ml_loglik;
Printf.printf "description length %.2f bits\n" d.Scoring.dl_bits;
Printf.printf "nonterminals %d\n" d.Scoring.num_nonterminals;
Printf.printf "productions %d\n" d.Scoring.num_productions
let cmd_parse opts =
let grammar_path = opt opts "grammar" "" in
let input = opt opts "input" "" in
if grammar_path = "" || input = "" then
(prerr_endline "parse: --grammar and --input are required"; exit 2);
let g = Grammar.normalize (Grammar.of_string (read_file grammar_path)) in
let tokens =
String.split_on_char ' ' (String.trim input)
|> List.filter (fun s -> String.length s > 0)
in
let prep = Parse.prepare g in
let p = Parse.inside prep tokens in
let vp, tree = Parse.viterbi prep tokens in
Printf.printf "tokens: %s\n" (String.concat " " tokens);
Printf.printf "P(string): %.10g\n" p;
Printf.printf "best parse: %.10g\n" vp;
(match tree with
| Some t -> Printf.printf "%s\n" (Parse.tree_to_string t)
| None -> print_endline "(no parse)");
(match List.assoc_opt "dot" opts with
| Some path -> (
match tree with
| Some t ->
Dot.write_file path (Dot.dot_tree ~title:"Viterbi parse" t);
Printf.printf "wrote %s\n" path
| None -> ())
| None -> ())
let cmd_dot opts =
let grammar_path = opt opts "grammar" "" in
let out = opt opts "out" "" in
if grammar_path = "" || out = "" then
(prerr_endline "dot: --grammar and --out are required"; exit 2);
let g = Grammar.normalize (Grammar.of_string (read_file grammar_path)) in
let title = opt opts "title" "grammar" in
let svg = List.assoc_opt "svg" opts in
let res =
Dot.render ?out_svg:svg ~out_dot:out (Dot.dot_grammar ~title g)
in
Printf.printf "wrote %s%s\n" out
(match res with Some s -> " and " ^ s | None -> "")
let cmd_experiments opts =
let out_dir = opt opts "out" "experiments" in
mkdir_p out_dir;
mkdir_p (Filename.concat out_dir "steps");
let render = flag opts "render" in
let quick = flag opts "quick" in
let config = config_of_opts opts in
let seed = int_opt opts "seed" 1 in
let beam = int_opt opts "beam" 3 in
let max_steps = int_opt opts "max-steps" (if quick then 12 else 25) in
let patience = int_opt opts "patience" (if quick then 3 else 5) in
let max_len = int_opt opts "max-len" (if quick then 6 else 7) in
let only = List.assoc_opt "only" opts in
let results =
Experiment.run_all
~export_dir:(if render then Some (Filename.concat out_dir "steps") else None)
~config ~beam_width:beam ~max_steps ~patience ~seed ~max_len ~only ()
in
if render then
List.iter
(fun (r : Experiment.result) ->
let base = Filename.concat out_dir r.Experiment.target.Experiment.name in
ignore
(Dot.render ~out_svg:(base ^ "-final.svg") ~out_dot:(base ^ "-final.dot")
(Dot.dot_grammar
~title:(Printf.sprintf "%s (induced)" r.Experiment.target.Experiment.name)
r.Experiment.final_grammar)))
results;
Dot.write_file (Filename.concat out_dir "results.tsv")
(Experiment.to_tsv results);
Dot.write_file (Filename.concat out_dir "report.md")
(Experiment.report_markdown results);
List.iter
(fun (r : Experiment.result) ->
let base = Filename.concat out_dir r.Experiment.target.Experiment.name in
Dot.write_file (base ^ "-final.dot")
(Dot.dot_grammar
~title:(Printf.sprintf "%s (induced)" r.Experiment.target.Experiment.name)
r.Experiment.final_grammar);
Dot.write_file (base ^ "-final.txt")
(Grammar.to_string r.Experiment.final_grammar))
results;
print_endline (Experiment.to_tsv results);
Printf.printf "\nwrote %s\n" out_dir
let () =
match Array.to_list Sys.argv with
| _ :: cmd :: rest -> (
let opts = parse_args rest in
match cmd with
| "induce" -> cmd_induce opts
| "score" -> cmd_score opts
| "parse" -> cmd_parse opts
| "dot" -> cmd_dot opts
| "experiments" -> cmd_experiments opts
| "help" | "-h" | "--help" -> usage ()
| other ->
Printf.eprintf "unknown command: %s\n\n" other;
usage ();
exit 2)
| _ -> usage ()