Packages

Text analysis and processing for Elixir including ngram, language detection and more.

Current section

Files

Jump to
text lib corpus.ex
Raw

lib/corpus.ex

defmodule Text.Corpus do
@moduledoc """
Defines the behaviour for a language
corpus with convenience functions to
simplifying the creation of corpus
vocabularies.
"""
@doc """
Returns a list of vocabularies for a corpus.
"""
@callback known_vocabularies :: [Text.vocabulary, ...]
@doc """
Returns a ist of vocabularies for a corpus.
"""
@callback known_languages :: [Text.language, ...]
@doc """
Returns the natural langauge training text for
a given language in the corpus.
"""
@callback language_content(Text.language) :: String.t
@doc """
Normalizes the text used for training and
for classification.
"""
@callback normalize_text(String.t) :: String.t
@doc """
Classifies the natural language of a given
text into an ordered list.
See `Text.Language.classify/2` for
the options that may be passed.
"""
@callback classify(String.t, Keyword.t) ::
[Text.frequency_tuple, ...] | {:error, {module(), String.t}}
@doc """
Detects the most likely natural language of a given
text.
See `Text.Language.detect/2` for
the options that may be passed.
"""
@callback detect(String.t, Keyword.t) ::
{:ok, Text.language} | {:error, {module(), String.t}}
@max_demand 5
@doc """
Builds the vocabulary for
all known vocabulary modules
"""
def build_vocabularies(corpus, options \\ []) do
max_demand = Keyword.get(options, :max_demand, @max_demand)
corpus.known_vocabularies()
|> Enum.each(&build_vocabulary(corpus, &1, max_demand: max_demand))
end
@doc """
Builds a vocabulary for a given vocanulary
module.
"""
def build_vocabulary(corpus, vocabulary, options \\ []) do
ngram_range = vocabulary.ngram_range()
file = vocabulary.filename()
max_demand = Keyword.get(options, :max_demand, @max_demand)
frequency_map_by_language =
corpus.known_languages()
|> Flow.from_enumerable(max_demand: max_demand)
|> Flow.map(&Text.Vocabulary.calculate_corpus_ngrams(corpus, &1, ngram_range))
|> Enum.to_list
|> calculate_global_frequencies
|> remove_structs_for_space_reduction
binary = :erlang.term_to_binary(frequency_map_by_language)
:ok = File.write!(file, binary)
frequency_map_by_language
end
@doc false
def remove_structs_for_space_reduction(frequency_map) do
Enum.map(frequency_map, fn {language, ngram_map} ->
new_ngram_map =
Enum.map(ngram_map, fn {ngram, stats} -> {ngram, Map.from_struct(stats)} end)
|> Map.new()
{language, new_ngram_map}
end)
|> Map.new()
end
# Calculate the total frequency for each
# ngram across all regions
@doc false
def calculate_global_frequencies(frequency_map_by_language) do
frequency_map_by_language
|> invert_to_frequency_map_by_ngram()
|> calculate_global_frequency_and_rank()
|> invert_to_frequency_map_by_language()
end
# Invert
# %{language => %{ngram => frequencies}}
# to:
# %{ngram => %{language => frequencies}}
@doc false
def invert_to_frequency_map_by_ngram(frequency_map_by_language) do
Enum.reduce(frequency_map_by_language, %{}, fn {language, ngrams}, acc ->
Enum.reduce(ngrams, acc, fn {ngram, ngram_stats}, acc2 ->
Map.update(acc2, ngram, %{language => ngram_stats}, &Map.put(&1, language, ngram_stats))
end)
end)
end
# Invert
# %{ngram => %{language => frequencies}}
# to:
# %{language => %{ngram => frequencies}}
@doc false
def invert_to_frequency_map_by_language(frequency_map_by_ngram) do
Enum.reduce(frequency_map_by_ngram, %{}, fn {ngram, languages}, acc ->
Enum.reduce(languages, acc, fn {language, ngram_stats}, acc2 ->
Map.update(acc2, language, %{ngram => ngram_stats}, &Map.put(&1, ngram, ngram_stats))
end)
end)
end
# Calculate the frequencies across all regions
# and then the global range across all region
@doc false
def calculate_global_frequency_and_rank(frequency_map_by_ngram) do
Enum.map(frequency_map_by_ngram, fn {ngram, ngram_by_language} ->
total_count_for_ngram = total_ngram_count_for_languages(ngram_by_language)
added_global_stats =
Enum.map(ngram_by_language, fn {language, ngram_stats} ->
{language, %{ngram_stats | global_frequency: ngram_stats.count / total_count_for_ngram}}
end)
|> Enum.sort(&(elem(&1, 1).global_frequency > elem(&2, 1).global_frequency))
|> Enum.with_index(1)
|> Enum.map(fn {{language, ngram_stats}, global_rank} ->
{language, %{ngram_stats | global_rank: global_rank}}
end)
{ngram, added_global_stats}
end)
end
@doc false
def total_ngram_count_for_languages(ngram_by_language) do
Enum.reduce(ngram_by_language, 0, fn {_language, %{count: count}}, acc ->
acc + count
end)
end
end