Natural Language Processing
The smile-nlp module covers the classical text-processing pipeline: Unicode normalization, paragraph / sentence / word splitting, stemming, part-of-speech tagging, dictionaries, collocation extraction, relevance ranking, word embeddings, and lightweight concept taxonomies.
This guide is for engineered NLP features and retrieval — not large language models. For LLM serving and generation APIs, see Large Language Models. Trainable sequence labelers (HMM / CRF) live on Sequence Labeling; classic bag-of-words classifiers are on Classification.
Module Contents
| Sub-package / Class | Description |
|---|---|
smile.nlp (core) | Text, Corpus, SimpleCorpus, Bigram, NGram, Word2Vec |
smile.util.Strings | Unicode text normalization (Strings.normalize) |
smile.nlp.tokenizer | Paragraph, sentence, and word splitting |
smile.nlp.stemmer | Porter and Lancaster word stemmers |
smile.nlp.pos | Penn Treebank POS tagging (pretrained HMM) |
smile.nlp.dictionary | Stop words, punctuations, English dictionary |
smile.nlp.relevance | TF-IDF and BM25 relevance ranking |
smile.nlp.taxonomy | Concept taxonomy and taxonomic distance |
Quick Start
A typical preprocessing path is normalize → paragraphs → sentences → tokens → POS tags → stems. Filter on PennTreebankPOS.open when you only want content words. Use one PorterStemmer per thread; the default HMM tagger is safe to share.
import smile.util.Strings;
import smile.nlp.tokenizer.*;
import smile.nlp.stemmer.PorterStemmer;
import smile.nlp.pos.*;
String raw = "Machine learning is a subfield of AI. Deep learning uses neural nets.";
String text = Strings.normalize(raw);
var paragraphs = SimpleParagraphSplitter.getInstance();
var sentences = SimpleSentenceSplitter.getInstance();
var tokenizer = new SimpleTokenizer();
var tagger = HMMPOSTagger.getDefault();
var stemmer = new PorterStemmer();
for (String para : paragraphs.split(text)) {
for (String sent : sentences.split(para)) {
String[] tokens = tokenizer.split(sent);
PennTreebankPOS[] tags = tagger.tag(tokens);
for (int i = 0; i < tokens.length; i++) {
if (tags[i].open) {
System.out.printf("%-16s %-6s %s%n",
tokens[i], tags[i], stemmer.stem(tokens[i].toLowerCase()));
}
}
}
}
Machine NN machin
learning NN learn
...
import smile.nlp._
import smile.nlp.pos.PennTreebankPOS
val raw = "Machine learning is a subfield of AI. Deep learning uses neural nets."
for (sent <- raw.normalize.sentences) {
val words = sent.words("none")
val tags = postag(words)
words.zip(tags).foreach { case (w, t) =>
if (t.open) println(f"$w%-16s $t%-6s ${porter.stem(w.toLowerCase)}")
}
}
Machine NN machin
learning NN learn
...
import smile.nlp.*
val raw = "Machine learning is a subfield of AI. Deep learning uses neural nets."
for (sent in raw.normalize().sentences()) {
val words = sent.words("none")
val tags = postag(words)
for (i in words.indices) {
if (tags[i].open) {
println("%-16s %-6s %s".format(
words[i], tags[i], porter(words[i].lowercase())))
}
}
}
Machine NN machin
learning NN learn
...
Core Types
The root smile.nlp package holds the document model, collocation records, an in-memory corpus, and word-embedding loaders.
Text
Text is a lightweight immutable record with optional title and body. Factory methods are Text.of(content) and Text.of(title, content).
import smile.nlp.Text;
Text doc = Text.of("Machine learning is a subfield of AI.");
Text titled = Text.of("Overview", "Machine learning is a subfield of AI.");
System.out.println(doc.content());
System.out.println(titled.title());
Machine learning is a subfield of AI.
Overview
import smile.nlp.Text
val doc = Text.of("Machine learning is a subfield of AI.")
val titled = Text.of("Overview", "Machine learning is a subfield of AI.")
println(doc.content)
println(titled.title)
Machine learning is a subfield of AI.
Overview
import smile.nlp.Text
val doc = Text.of("Machine learning is a subfield of AI.")
val titled = Text.of("Overview", "Machine learning is a subfield of AI.")
println(doc.content())
println(titled.title())
Machine learning is a subfield of AI.
Overview
Bigram and NGram
Bigram and NGram are records for word pairs and n-word phrases, optionally carrying raw frequency and association scores. They are the return types of collocation extractors.
import smile.nlp.Bigram;
import smile.nlp.NGram;
Bigram b = new Bigram("machine", "learning");
NGram n = new NGram(new String[]{"deep", "learning"});
System.out.println(b);
System.out.println(n);
Bigram{w1='machine', w2='learning', ...}
NGram{words=[deep, learning], ...}
SimpleCorpus
Corpus is the indexed-document interface. SimpleCorpus is the built-in in-memory implementation: it tokenizes on insert and keeps term / bigram tables for collocations and relevance search. Scala and Kotlin helpers build a corpus from a sequence of strings.
import smile.nlp.SimpleCorpus;
SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc("SMILE is a fast machine learning library."));
corpus.add(corpus.doc("Deep learning uses neural networks."));
System.out.println(corpus.docCount());
System.out.println(corpus.count("learning"));
2
2
import smile.nlp._
val c = corpus(Seq(
"SMILE is a fast machine learning library.",
"Deep learning uses neural networks."
))
println(c.docCount)
println(c.count("learning"))
2
2
import smile.nlp.*
val c = corpus(listOf(
"SMILE is a fast machine learning library.",
"Deep learning uses neural networks."
))
println(c.docCount())
println(c.count("learning"))
2
2
Word2Vec
Word2Vec loads pretrained embeddings in Word2vec binary format (Word2Vec.of) or GloVe text format (Word2Vec.glove). Look up vectors with apply or lookup, and compare tokens with cosine similarity.
import java.nio.file.Path;
import smile.nlp.Word2Vec;
// Word2vec binary or GloVe text
Word2Vec w2v = Word2Vec.of(Path.of("GoogleNews-vectors-negative300.bin"));
// Word2Vec glove = Word2Vec.glove(Path.of("glove.6B.100d.txt"));
float[] vec = w2v.apply("king");
w2v.lookup("king").ifPresent(v -> System.out.println(v.length));
w2v.similarity("king", "queen").ifPresent(System.out::println);
System.out.println(w2v.size() + " x " + w2v.dimension());
300
≈ 0.65
(vocabulary size) x 300
Normalizer
Strings.normalize cleans heterogeneous Unicode before tokenization. Always normalize text from the web, PDFs, or user input to avoid spurious tokens from encoding variants.
| Step | Detail |
|---|---|
| NFKC | Canonical decomposition followed by composition |
| Whitespace | Strip, trim, and compress whitespace |
| Control chars | Remove Cc and Cf categories |
| Dashes | Em-dash, en-dash, etc. → - |
| Double quotes | Typographic quotes → " |
| Single quotes | Typographic apostrophes → ' |
import smile.util.Strings;
String clean = Strings.normalize("\u201CHello,\u201D world\u2019s\u2026");
System.out.println(clean);
"Hello," world's...
import smile.nlp._
val clean = "\u201CHello,\u201D world\u2019s\u2026".normalize
println(clean)
"Hello," world's...
import smile.nlp.*
val clean = "\u201CHello,\u201D world\u2019s\u2026".normalize()
println(clean)
"Hello," world's...
Tokenizers
The tokenizer package splits text at three levels. All splitter interfaces extend Function<String, String[]>, so they compose in streams. Prefer the Simple* and PennTreebank singletons for English; BreakIterator* variants are locale-aware but not thread-safe.
| Class | Level | Notes |
|---|---|---|
SimpleParagraphSplitter | Paragraph | Blank lines; singleton |
SimpleSentenceSplitter | Sentence | English heuristics + abbreviations; singleton |
BreakIteratorSentenceSplitter | Sentence | Locale-aware; not thread-safe |
SimpleTokenizer | Word | Expands contractions; thread-safe |
PennTreebankTokenizer | Word | Penn Treebank conventions; singleton |
BreakIteratorTokenizer | Word | Locale-aware; not thread-safe |
Scala and Kotlin enrich strings with sentences and words(filter). The filter may be none, default, comprehensive, google, mysql, or a comma-separated custom stop list.
import smile.nlp.tokenizer.*;
String text = "Dr. Smith lives in the U.S. He works at NASA.";
String[] sents = SimpleSentenceSplitter.getInstance().split(text);
Tokenizer tok = new SimpleTokenizer();
for (String s : sents) {
System.out.println(String.join(" | ", tok.split(s)));
}
Dr. | Smith | lives | in | the | U.S. | .
He | works | at | NASA | .
import smile.nlp._
val text = "Dr. Smith lives in the U.S. He works at NASA."
text.sentences.foreach { s =>
println(s.words("none").mkString(" | "))
}
Dr. | Smith | lives | in | the | U.S. | .
He | works | at | NASA | .
import smile.nlp.*
val text = "Dr. Smith lives in the U.S. He works at NASA."
for (s in text.sentences()) {
println(s.words("none").joinToString(" | "))
}
Dr. | Smith | lives | in | the | U.S. | .
He | works | at | NASA | .
Choosing an Implementation
| Word tokenizer scenario | Recommended |
|---|---|
| General English text | SimpleTokenizer |
| Penn Treebank / pretrained NLP models | PennTreebankTokenizer |
| Non-English or multilingual | BreakIteratorTokenizer |
| Sentence splitter scenario | Recommended |
|---|---|
| English text (production) | SimpleSentenceSplitter |
| Multilingual / locale-sensitive | BreakIteratorSentenceSplitter |
| Paragraph splitter scenario | Recommended |
|---|---|
| Blank-line paragraph boundaries | SimpleParagraphSplitter |
Thread Safety
| Class | Thread-safe? | Notes |
|---|---|---|
SimpleTokenizer | Yes | Stateless after construction |
PennTreebankTokenizer | Yes | Stateless singleton |
BreakIteratorTokenizer | No | Use one instance per thread (ThreadLocal) |
SimpleSentenceSplitter | Yes | Stateless singleton |
BreakIteratorSentenceSplitter | No | Use one instance per thread |
SimpleParagraphSplitter | Yes | Stateless singleton |
Penn Treebank vs SimpleTokenizer
Package-private EnglishAbbreviations backs the English splitters: SimpleSentenceSplitter and both word tokenizers consult it so titles and Latin abbreviations (Dr., etc., U.S., …) are not treated as sentence or token boundaries. Prefer PennTreebankTokenizer.getInstance() when feeding HMMPOSTagger (trained on Treebank conventions); SimpleTokenizer is the general-purpose default.
import java.util.Arrays;
import smile.nlp.tokenizer.*;
String s = "They couldn't have known.";
Tokenizer simple = new SimpleTokenizer();
Tokenizer ptb = PennTreebankTokenizer.getInstance();
System.out.println(Arrays.toString(simple.split(s)));
System.out.println(Arrays.toString(ptb.split(s)));
[They, couldn't, have, known, .]
[They, could, n't, have, known, .]
import smile.nlp.tokenizer._
val s = "They couldn't have known."
println(new SimpleTokenizer().split(s).mkString("[", ", ", "]"))
println(PennTreebankTokenizer.getInstance.split(s).mkString("[", ", ", "]"))
[They, couldn't, have, known, .]
[They, could, n't, have, known, .]
import smile.nlp.tokenizer.*
val s = "They couldn't have known."
println(SimpleTokenizer().split(s).contentToString())
println(PennTreebankTokenizer.getInstance().split(s).contentToString())
[They, couldn't, have, known, .]
[They, could, n't, have, known, .]
Stemmers
Both stemmers implement Stemmer extends Function<String,String> for stream use. Porter is moderate and not thread-safe (one instance per thread). Lancaster is aggressive and shareable; pass true to also strip prefixes.
| Class | Algorithm | Aggressiveness | Thread-safe? |
|---|---|---|---|
PorterStemmer | Porter 1980 | Moderate | No (per-thread) |
LancasterStemmer | Paice/Husk 1990 | High | Yes |
import smile.nlp.stemmer.*;
PorterStemmer porter = new PorterStemmer();
LancasterStemmer lancaster = new LancasterStemmer();
System.out.println(porter.stem("generalization"));
System.out.println(lancaster.stem("presumably"));
gener
presum
import smile.nlp._
println(porter.stem("generalization"))
println(lancaster.stem("presumably"))
gener
presum
import smile.nlp.*
println(porter("generalization"))
println(lancaster("presumably"))
gener
presum
Porter vs Lancaster
| Criterion | Porter | Lancaster |
|---|---|---|
| Stem readability | Often recognizable | Often fragments |
| Conflation strength | Moderate | High |
| Typical use | IR, topic models, search | Aggressive dedup / small vocabularies |
| Thread safety | One instance per thread | Safe to share after construction |
Start with PorterStemmer. Switch to LancasterStemmer when vocabulary size is the bottleneck. Porter does not lower-case input; Lancaster lower-cases and strips non-letters automatically.
stripPluralParticiple
PorterStemmer.stripPluralParticiple applies only the plural / participle step (Porter step 1a/1b-style), useful when you want light normalization without full stemming. Short words such as was, has, his, and this are preserved.
import smile.nlp.stemmer.PorterStemmer;
PorterStemmer porter = new PorterStemmer();
System.out.println(porter.stripPluralParticiple("cats"));
System.out.println(porter.stripPluralParticiple("running"));
System.out.println(porter.stripPluralParticiple("was"));
System.out.println(porter.stem("generalization"));
cat
run
was
gener
import smile.nlp.stemmer.PorterStemmer
val porter = new PorterStemmer
println(porter.stripPluralParticiple("cats"))
println(porter.stripPluralParticiple("running"))
println(porter.stripPluralParticiple("was"))
println(porter.stem("generalization"))
cat
run
was
gener
import smile.nlp.stemmer.PorterStemmer
val porter = PorterStemmer()
println(porter.stripPluralParticiple("cats"))
println(porter.stripPluralParticiple("running"))
println(porter.stripPluralParticiple("was"))
println(porter.stem("generalization"))
cat
run
was
gener
Lancaster Prefix Stripping
Pass true to strip scientific prefixes (kilo, micro, milli, intra, ultra, mega, nano, pico, pseudo). You can also supply a custom Lancaster rule file via new LancasterStemmer(InputStream) or new LancasterStemmer(InputStream, boolean).
import smile.nlp.stemmer.LancasterStemmer;
LancasterStemmer lancaster = new LancasterStemmer(true);
System.out.println(lancaster.stem("microprocessor"));
System.out.println(lancaster.stem("kilowatt"));
System.out.println(lancaster.stem("pseudoscience"));
process
wat
scy
import smile.nlp.stemmer.LancasterStemmer
val lancaster = new LancasterStemmer(true)
println(lancaster.stem("microprocessor"))
println(lancaster.stem("kilowatt"))
println(lancaster.stem("pseudoscience"))
process
wat
scy
import smile.nlp.stemmer.LancasterStemmer
val lancaster = LancasterStemmer(true)
println(lancaster.stem("microprocessor"))
println(lancaster.stem("kilowatt"))
println(lancaster.stem("pseudoscience"))
process
wat
scy
Part-of-Speech Tagging
HMMPOSTagger.getDefault() loads a pretrained first-order HMM over the Penn Treebank tag set. PennTreebankPOS.open marks content (open-class) tags. Package-private RegexPOSTagger is the OOV pre-classifier used inside the HMM for numbers, URLs, and e-mails. To train general HMM / CRF sequence models, see Sequence Labeling.
| Class | Role |
|---|---|
PennTreebankPOS | 45-tag enum; open distinguishes content vs function words |
HMMPOSTagger | Pretrained HMM tagger; serializable |
RegexPOSTagger | Package-private OOV rules (numbers, URLs, e-mails) |
EnglishPOSLexicon | ~200k word lexicon (Moby + WordNet) |
import smile.nlp.pos.*;
HMMPOSTagger tagger = HMMPOSTagger.getDefault();
String[] tokens = {"The", "cat", "sat", "on", "the", "mat", "."};
PennTreebankPOS[] tags = tagger.tag(tokens);
for (int i = 0; i < tokens.length; i++) {
System.out.printf("%-12s %s%n", tokens[i], tags[i]);
}
The DT
cat NN
sat VBD
on IN
the DT
mat NN
. .
import smile.nlp._
val tokens = Array("The", "cat", "sat", "on", "the", "mat", ".")
tokens.zip(postag(tokens)).foreach { case (w, t) =>
println(f"$w%-12s $t")
}
The DT
cat NN
sat VBD
on IN
the DT
mat NN
. .
import smile.nlp.*
val tokens = arrayOf("The", "cat", "sat", "on", "the", "mat", ".")
val tags = postag(tokens)
for (i in tokens.indices) {
println("%-12s %s".format(tokens[i], tags[i]))
}
The DT
cat NN
sat VBD
on IN
the DT
mat NN
. .
Open vs Closed Class
Open-class tags (nouns, verbs, adjectives, adverbs, …) have open == true and are the usual candidates for bag-of-words and keyword features. Closed-class tags (determiners, prepositions, pronouns, …) have open == false. PennTreebankPOS.getValue(String) parses corpus tag strings, including punctuation symbols mapped to named constants (e.g. "." → SENT).
import smile.nlp.pos.PennTreebankPOS;
PennTreebankPOS nn = PennTreebankPOS.getValue("NN");
PennTreebankPOS dot = PennTreebankPOS.getValue(".");
System.out.println(nn + " open=" + nn.open);
System.out.println(dot + " open=" + dot.open);
System.out.println(PennTreebankPOS.DT.open);
NN open=true
. open=false
false
RegexPOSTagger (internal OOV rules)
RegexPOSTagger is package-private and used by HMMPOSTagger for out-of-vocabulary tokens. Its tag(String) returns an Optional<PennTreebankPOS>: cardinal / comma-formatted numbers → CD; phone numbers, URLs, and e-mails → NN; empty when no pattern matches.
EnglishPOSLexicon
EnglishPOSLexicon.get(word) looks up ~200k English words (Moby + WordNet) and returns an Optional of candidate Penn tags in priority order.
import java.util.Arrays;
import smile.nlp.pos.EnglishPOSLexicon;
EnglishPOSLexicon.get("run").ifPresent(tags -> {
System.out.println("primary=" + tags[0]);
System.out.println(Arrays.toString(tags));
});
System.out.println(EnglishPOSLexicon.get("xyzzy").isEmpty());
primary=VB
[VB, VB, VB, NN]
true
Custom HMM POS Model
Train a domain tagger with HMMPOSTagger.fit on aligned token / tag arrays. On the bundled WSJ Treebank data, 10-fold CV error is about 5 %. Package-private HMMPOSTagger.read loads *.POS files (word/TAG lines) when you train from inside the module; otherwise build the arrays yourself. For general HMM / CRF training APIs, see Sequence Labeling.
import smile.nlp.pos.*;
// sentences: String[][] tokens; labels: PennTreebankPOS[][]
// HMMPOSTagger.read(dir, sentences, labels); // package-private helper
String[][] x = {
{"The", "cat", "sat", "."},
{"Dogs", "bark", "."}
};
PennTreebankPOS[][] y = {
{PennTreebankPOS.DT, PennTreebankPOS.NN, PennTreebankPOS.VBD, PennTreebankPOS.SENT},
{PennTreebankPOS.NNS, PennTreebankPOS.VBP, PennTreebankPOS.SENT}
};
HMMPOSTagger custom = HMMPOSTagger.fit(x, y);
System.out.println(java.util.Arrays.toString(custom.tag(x[0])));
[DT, NN, VBD, .]
Dictionaries
Ready-made English word lists live in smile.nlp.dictionary. Use them to filter tokens before bag-of-words or keyword extraction.
EnglishStopWords
| Constant | Coverage |
|---|---|
EnglishStopWords.DEFAULT | Standard list (~500 words) |
EnglishStopWords.COMPREHENSIVE | Extended list (thousands) |
EnglishStopWords.GOOGLE | Google stop-word list |
EnglishStopWords.MYSQL | MySQL FullText stop-word list |
import smile.nlp.dictionary.EnglishStopWords;
EnglishStopWords stop = EnglishStopWords.DEFAULT;
System.out.println(stop.contains("the"));
System.out.println(stop.contains("machine"));
true
false
import smile.nlp.dictionary.EnglishStopWords
val stop = EnglishStopWords.DEFAULT
println(stop.contains("the"))
println(stop.contains("machine"))
true
false
import smile.nlp.dictionary.EnglishStopWords
val stop = EnglishStopWords.DEFAULT
println(stop.contains("the"))
println(stop.contains("machine"))
true
false
EnglishPunctuations
import smile.nlp.dictionary.EnglishPunctuations;
EnglishPunctuations punct = EnglishPunctuations.getInstance();
System.out.println(punct.contains("."));
System.out.println(punct.contains("NLP"));
true
false
import smile.nlp.dictionary.EnglishPunctuations
val punct = EnglishPunctuations.getInstance
println(punct.contains("."))
println(punct.contains("NLP"))
true
false
import smile.nlp.dictionary.EnglishPunctuations
val punct = EnglishPunctuations.getInstance()
println(punct.contains("."))
println(punct.contains("NLP"))
true
false
EnglishDictionary
EnglishDictionary is an enum with a membership test over a loaded word list. Use the CONCISE constant — there is no static EnglishDictionary.contains.
import smile.nlp.dictionary.EnglishDictionary;
System.out.println(EnglishDictionary.CONCISE.contains("serendipity"));
System.out.println(EnglishDictionary.CONCISE.contains("xyzzy"));
true
false
import smile.nlp.dictionary.EnglishDictionary
println(EnglishDictionary.CONCISE.contains("serendipity"))
println(EnglishDictionary.CONCISE.contains("xyzzy"))
true
false
import smile.nlp.dictionary.EnglishDictionary
println(EnglishDictionary.CONCISE.contains("serendipity"))
println(EnglishDictionary.CONCISE.contains("xyzzy"))
true
false
Collocations
Collocations are statistically significant word co-occurrences. SimpleCorpus exposes two bigram overloads ranked by log-likelihood ratio (−2 log λ, χ² with 1 df): bigrams(k, minFreq) returns the top-k pairs, and bigrams(p, minFreq) returns all pairs whose p-value is below p (0 < p < 1). minFrequency is exclusive (count > minFrequency). NGram.apriori mines frequent phrases of length up to maxN from already-tokenized sentences.
import smile.nlp.SimpleCorpus;
SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc("Machine learning is part of artificial intelligence."));
corpus.add(corpus.doc("Deep learning is a type of machine learning."));
corpus.add(corpus.doc("Neural networks power deep learning systems."));
System.out.println("--- top-k ---");
corpus.bigrams(5, 1).forEach(b ->
System.out.println(b.w1() + " " + b.w2() + " score=" + b.score()));
System.out.println("--- p-value ---");
corpus.bigrams(0.01, 1).forEach(b ->
System.out.println(b.w1() + " " + b.w2() + " score=" + b.score()));
--- top-k ---
machine learning score=...
--- p-value ---
...
import smile.nlp._
val docs = Seq(
"Machine learning is part of artificial intelligence.",
"Deep learning is a type of machine learning.",
"Neural networks power deep learning systems.")
println("--- top-k ---")
bigram(5, 1, docs: _*).foreach(b => println(s"${b.w1} ${b.w2} score=${b.score}"))
println("--- p-value ---")
bigram(0.01, 1, docs: _*).foreach(b => println(s"${b.w1} ${b.w2} score=${b.score}"))
--- top-k ---
machine learning score=...
--- p-value ---
...
import smile.nlp.*
val docs = listOf(
"Machine learning is part of artificial intelligence.",
"Deep learning is a type of machine learning.",
"Neural networks power deep learning systems."
)
println("--- top-k ---")
for (b in bigrams(5, 1, docs)) {
println("${b.w1()} ${b.w2()} score=${b.score()}")
}
println("--- p-value ---")
for (b in bigrams(0.01, 1, docs)) {
println("${b.w1()} ${b.w2()} score=${b.score()}")
}
--- top-k ---
machine learning score=...
--- p-value ---
...
Parameter Tuning
| Parameter | Guidance |
|---|---|
minFrequency | Exclusive lower bound. Tiny corpora: 1–2; 1k–10k tokens: 2–5; 10k–100k: 5–10; larger: 10–50 |
k (top-k) | Fixed-size feature / display lists; start around 20 |
p | 0.05 lenient; 0.01 / 0.001 stricter; 0.0001 very strict |
For n-grams longer than two, tokenize sentences first, then call NGram.apriori(sentences, maxN, minFreq). Index result[n] holds the n-grams (sorted by descending count). Scala / Kotlin ngram helpers accept raw text and handle sentence splitting and stemming internally.
import java.util.List;
import smile.nlp.NGram;
import smile.nlp.tokenizer.*;
String[] docs = {
"Machine learning is part of artificial intelligence.",
"Deep learning is a type of machine learning."
};
var splitter = SimpleSentenceSplitter.getInstance();
var tokenizer = new SimpleTokenizer();
List<String[]> sentences = java.util.Arrays.stream(docs)
.flatMap(d -> java.util.Arrays.stream(splitter.split(d)))
.map(tokenizer::split)
.toList();
NGram[][] result = NGram.apriori(sentences, 3, 2);
for (NGram g : result[2]) {
System.out.println(String.join(" ", g.words()) + " count=" + g.count());
}
machin learn count=...
import smile.nlp._
val result = ngram(3, 2,
"Machine learning is part of artificial intelligence.",
"Deep learning is a type of machine learning.")
result(2).foreach(g => println(g.words.mkString(" ") + " count=" + g.count))
machin learn count=...
import smile.nlp.*
val result = ngram(3, 2, listOf(
"Machine learning is part of artificial intelligence.",
"Deep learning is a type of machine learning."
))
for (g in result[2]) {
println(g.words().joinToString(" ") + " count=" + g.count())
}
machin learn count=...
Keywords
Text.keywords(maxNum) extracts top phrases from a single document via word co-occurrence statistics (Matsuo & Ishizuka). Scala and Kotlin expose the same algorithm as string extensions.
import smile.nlp.Text;
String body = "Machine learning is a field of artificial intelligence. "
+ "Deep learning is a subfield of machine learning that uses neural networks. "
+ "Neural networks learn hierarchical representations from data.";
Text.of(body).keywords(5).forEach(k ->
System.out.println(String.join(" ", k.words()) + " count=" + k.count()));
machin learn count=...
deep learn count=...
import smile.nlp._
val body =
"""Machine learning is a field of artificial intelligence.
Deep learning is a subfield of machine learning that uses neural networks.
Neural networks learn hierarchical representations from data."""
body.keywords(5).foreach(k =>
println(k.words.mkString(" ") + " count=" + k.count))
machin learn count=...
deep learn count=...
import smile.nlp.*
val body = """
Machine learning is a field of artificial intelligence.
Deep learning is a subfield of machine learning that uses neural networks.
Neural networks learn hierarchical representations from data.
""".trimIndent()
for (k in body.keywords(5)) {
println(k.words().joinToString(" ") + " count=" + k.count())
}
machin learn count=...
deep learn count=...
Relevance Ranking
Plug a RelevanceRanker into Corpus.search to rank documents for a query string or token array. TFIDF is classic weighted bag-of-words; BM25 is Okapi BM25+ for ad-hoc retrieval. Default new BM25() uses k1=1.2, b=0.75, delta=1.0; tune with new BM25(k1, b, delta) (set delta=0 for classic BM25 without the BM25+ floor).
| Criterion | TF-IDF | BM25 |
|---|---|---|
| Simplicity | Straightforward formula | Slightly more parameters |
| Long documents | Bias toward long docs | Length-normalized by b |
| TF saturation | None (linear in tf) | Logarithmic via k1 |
| Tunable | One parameter (a) | Three (k1, b, delta) |
| Recommended for | Prototypes, small corpora | Production search, larger corpora |
import smile.nlp.Text;
import smile.nlp.SimpleCorpus;
import smile.nlp.relevance.*;
SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc(Text.of("Intro", "SMILE is a fast machine learning library.")));
corpus.add(corpus.doc(Text.of("DL", "Deep learning uses neural networks.")));
corpus.add(corpus.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")));
RelevanceRanker bm25 = new BM25();
corpus.search(bm25, new String[]{"machine", "learning"})
.forEachRemaining(r ->
System.out.printf("%.4f %s%n", r.score(), r.text().title()));
0.xxxx Intro
...
import smile.nlp._
import smile.nlp.Text
import smile.nlp.relevance.BM25
import scala.jdk.CollectionConverters._
val c = new SimpleCorpus
c.add(c.doc(Text.of("Intro", "SMILE is a fast machine learning library.")))
c.add(c.doc(Text.of("DL", "Deep learning uses neural networks.")))
c.add(c.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")))
c.search(new BM25, Array("machine", "learning")).asScala.foreach { r =>
println(f"${r.score}%.4f ${r.text.title}")
}
0.xxxx Intro
...
import smile.nlp.*
import smile.nlp.relevance.BM25
val c = SimpleCorpus()
c.add(c.doc(Text.of("Intro", "SMILE is a fast machine learning library.")))
c.add(c.doc(Text.of("DL", "Deep learning uses neural networks.")))
c.add(c.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")))
c.search(BM25(), arrayOf("machine", "learning")).forEachRemaining { r ->
println("%.4f %s".format(r.score(), r.text().title()))
}
0.xxxx Intro
...
TF-IDF Side by Side
Same corpus, same query — swap the ranker to compare scores. Start with new BM25() for production; use TFIDF when you want a simpler baseline.
import smile.nlp.Text;
import smile.nlp.SimpleCorpus;
import smile.nlp.relevance.*;
SimpleCorpus corpus = new SimpleCorpus();
corpus.add(corpus.doc(Text.of("Intro", "SMILE is a fast machine learning library.")));
corpus.add(corpus.doc(Text.of("DL", "Deep learning uses neural networks.")));
corpus.add(corpus.doc(Text.of("IR", "BM25 ranks documents for information retrieval.")));
String[] q = {"machine", "learning"};
System.out.println("BM25:");
corpus.search(new BM25(), q).forEachRemaining(r ->
System.out.printf(" %.4f %s%n", r.score(), r.text().title()));
System.out.println("TF-IDF:");
corpus.search(new TFIDF(), q).forEachRemaining(r ->
System.out.printf(" %.4f %s%n", r.score(), r.text().title()));
BM25:
0.xxxx Intro
...
TF-IDF:
0.xxxx Intro
...
Taxonomy
A Taxonomy is an ordered IS-A tree of Concept nodes. Each concept carries a synonym set via keywords(). TaxonomicDistance provides edge-counting distance d(), normalized distance, and three similarity measures (Wu–Palmer, Leacock–Chodorow, Lin). d() counts edges through the LCA — it is not Wu–Palmer; call wuPalmer separately for that similarity.
Parsing with Taxonomy.of
Taxonomy.of(String) builds a tree from indented text: 4 spaces (or one tab) per level, comma-separated synonyms on a line, # comments allowed. The first non-blank line is the root.
import smile.nlp.taxonomy.*;
Taxonomy taxonomy = Taxonomy.of("""
animal
mammal, warm-blooded
dog, canine
cat, feline
reptile
snake
""");
System.out.println(taxonomy);
System.out.println(taxonomy.getConcept("dog") == taxonomy.getConcept("canine"));
System.out.println(taxonomy.getConcept("dog").keywords());
[animal]
├── [mammal, warm-blooded]
│ ├── [canine, dog]
│ └── [cat, feline]
└── [reptile]
└── [snake]
true
[canine, dog]
Depth, LCA, and Ancestors
import smile.nlp.taxonomy.*;
Taxonomy t = Taxonomy.of("""
animal
mammal, warm-blooded
dog, canine
cat, feline
reptile
snake
""");
System.out.println("depth(dog)=" + t.depth("dog"));
System.out.println("LCA(dog,snake)=" + t.lowestCommonAncestor("dog", "snake"));
System.out.println("isAncestor(mammal,dog)=" + t.isAncestor("mammal", "dog"));
System.out.println("height=" + t.height() + " size=" + t.size());
depth(dog)=2
LCA(dog,snake)=Concept [animal]
isAncestor(mammal,dog)=true
height=2 size=9
TaxonomicDistance Measures
| Method | Kind | Notes |
|---|---|---|
d | Distance | Edge count via LCA: depth(a)+depth(b)−2·depth(LCA) |
normalizedDistance | Distance | Raw distance / diameter ∈ [0, 1] |
wuPalmer | Similarity | 2·depth(LCA)/(depth(a)+depth(b)) |
leacockChodorow | Similarity | Path length vs taxonomy height, normalized to [0, 1] |
lin | Similarity | Depth-proxy information content; no external corpus |
import smile.nlp.taxonomy.*;
Taxonomy t = Taxonomy.of("""
animal
mammal, warm-blooded
dog, canine
cat, feline
reptile
snake
""");
TaxonomicDistance td = new TaxonomicDistance(t);
System.out.printf("d(dog,cat)=%.1f%n", td.d("dog", "cat"));
System.out.printf("normalized=%.3f%n", td.normalizedDistance("dog", "cat"));
System.out.printf("wuPalmer=%.3f%n", td.wuPalmer("dog", "cat"));
System.out.printf("leacockChodorow=%.3f%n", td.leacockChodorow("dog", "cat"));
System.out.printf("wuPalmer(dog,snake)=%.3f%n", td.wuPalmer("dog", "snake"));
System.out.printf("lin(dog,cat)=%.3f%n", td.lin("dog", "cat"));
d(dog,cat)=2.0
normalized=0.500
wuPalmer=0.500
leacockChodorow=0.500
wuPalmer(dog,snake)=0.000
lin(dog,cat)=0.000
Package Map
smile.nlp
├── Text, Corpus, SimpleCorpus – document model & in-memory corpus
├── Bigram, NGram – collocation data records
├── Word2Vec – word embeddings (Word2vec / GloVe)
│
├── tokenizer/ Paragraph / Sentence / Word splitters
├── stemmer/ PorterStemmer, LancasterStemmer
├── pos/ HMMPOSTagger, PennTreebankPOS, RegexPOSTagger
├── dictionary/ EnglishStopWords, EnglishPunctuations, EnglishDictionary
├── relevance/ TFIDF, BM25
└── taxonomy/ Concept, Taxonomy, TaxonomicDistance