Ruby製の英単語の原形を探すライブラリlemmatizerのソースコードを読んでいます。
レポジトリーはこちらです。
読み込まれる辞書データは次のような形式になっています。
不規則活用の辞書では、”不規則活用形 原形”で各行が並んでいます。
# noun.exc aardwolves aardwolf abaci abacus aboideaux aboideau aboiteaux aboiteau abscissae abscissa
index.品詞のファイルでは、規則変化、不規則変化に関わらず、単語のリストが並んでいます。この辞書で活用されるのは、最初の1単語のみです。
# index.noun acculturation n 3 3 @ ~ + 3 1 01128984 05984936 05757049 accumulation n 4 4 @ ~ + ; 4 3 13424865 07951464 00372013 13366693 accumulator n 3 4 @ ~ %p ; 3 0 09936362 04328329 02673078 accumulator_register n 1 2 @ ; 1 0 02673078 accuracy n 2 5 ! @ ~ = ; 2 2 04802907 04803209
この辞書は、PythonのNLTKライブラリのWordnetの辞書から借用されているようなので、
Wordnetの他の相関データがそのまま入っていて、
ここの辞書データの全てを利用している訳ではないのでは?と推測しています。
以下、ソースコードを読み解きながら、どのようにlemmaメソッドが動作しているかを解読しようと試みます。
まずは、ライブラリを管理するlemmatizer.gemspec
です。以下の箇所では、Gemをrequireしたとき、実際にロードするファイルのパスが指定されています。libフォルダ以下にあるファイル群が読み込まれます。
Gem::Specification.new do |gem| (省略) gem.require_paths = ['lib'] end
lib/lemmatizer.rb
では、次の順でモジュールが読み込まれます。
require 'stringio' require 'lemmatizer/version' require 'lemmatizer/core_ext' require 'lemmatizer/lemmatizer' # asmさんからの助言 # lem = Lemmatizer::Lemmatizer.new と書くのがめんどくさいから # lem = Lemmatizer.new と書けるようにしている module Lemmatizer def self.new(dict = nil) Lemmatizer.new(dict) end end
次は、'lemmatizer/lemmatizer.rb'のコードです。
module Lemmatizer class Lemmatizer #########################辞書データの作成##################################### # 辞書データがディレクトリのPATH # 大文字で始まる場合は「定数」。各メソッドから参照可能。 DATA_DIR = File.expand_path('..', File.dirname(__FILE__)) # 辞書データのPATH # noun(名詞)、verb(動詞)、adj(形容詞)、adv(副詞) # index.品詞は見出し語。品詞.excは不規則活用。excはexception(例外)。 WN_FILES = { :noun => [ DATA_DIR + '/dict/index.noun', DATA_DIR + '/dict/noun.exc' ], :verb => [ DATA_DIR + '/dict/index.verb', DATA_DIR + '/dict/verb.exc' ], :adj => [ DATA_DIR + '/dict/index.adj', DATA_DIR + '/dict/adj.exc' ], :adv => [ DATA_DIR + '/dict/index.adv', DATA_DIR + '/dict/adv.exc' ] } # morphological substitution (形態論の置き換え) # 規則的に置き換え可能な場合のルール # 重複するものは、ing, es, ed, er, est。 MORPHOLOGICAL_SUBSTITUTIONS = { :noun => [ ['s', '' ], ['ses', 's' ], ['ves', 'f' ], ['xes', 'x' ], ['zes', 'z' ], ['ches', 'ch' ], ['shes', 'sh' ], ['men', 'man'], ['ies', 'y' ] ], :verb => [ ['s', '' ], ['ies', 'y'], ['es', 'e'], ['es', '' ], ['ed', 'e'], ['ed', '' ], ['ing', 'e'], ['ing', '' ] ], :adj => [ ['er', '' ], ['est', '' ], ['er', 'e'], ['est', 'e'] ], :adv => [ ], :abbr => [ ], :unknown => [ ] } # @wordlistsと@exceptionsに辞書データを登録する def load_wordnet_files(pos, list, exc) # 実行前、@wordlistsと@exceptionsは次のような構造。これらにデータを登録する。 # {:noun=>{}, :verb=>{}, :adj=>{}, :adv=>{}, :abbr=>{}, :unknown=>{}} # 見出し語の登録 # "acculturation"での例 # w = "acculturation n 3 3 @~省略~".split(/\s+/)[0] # w は "acculturation" # wordlists[:noun]["acculturation"] = "acculturation" open_file(list) do |io| io.each_line do |line| w = line.split(/\s+/)[0] @wordlists[pos][w] = w end end # 例外語の登録 # 例外語の辞書の各行は、"活用形 原形"(went go)の形式 # 活用形をwに、原形をsとして、ハッシュに追加していく # @exceptions[pos][w]が空ならば[]を代入する # @exceptions[pos][w]に、原形をpush << する。 open_file(exc) do |io| io.each_line do |line| w, s = line.split(/\s+/) @exceptions[pos][w] ||= [] @exceptions[pos][w] << s end end end # インスタンスの初期化の際、次のように呼び出される # WN_FILES.each_pair do |pos, pair| # load_wordnet_files(pos, pair[0], pair[1]) # end # # WN_FILESは、{品詞 => [index.品詞, 品詞.例外]}を持つハッシュ # WN_FILES = { # :noun => [ # DATA_DIR + '/dict/index.noun', # DATA_DIR + '/dict/noun.exc' # ], # # よって、pair[0]は見出し語、pair[1]は例外語を示す。 # load_wordnet_files(pos, pair[0], pair[1]) def load_provided_dict(dict) num_lex_added = 0 open_file(dict) do |io| io.each_line do |line| # pos must be either n|v|r|a or noun|verb|adverb|adjective p, w, s = line.split(/\s+/, 3) pos = str_to_pos(p) word = w substitute = s.strip if /\A\"(.*)\"\z/ =~ substitute substitute = $1 end if /\A\'(.*)\'\z/ =~ substitute substitute = $1 end next unless (pos && word && substitute) if @wordlists[pos] @wordlists[pos][word] = substitute num_lex_added += 1 end end end # puts "#{num_lex_added} items added from #{File.basename dict}" end #########################辞書データを検索##################################### def lemma(form, pos = nil) unless pos [:verb, :noun, :adj, :adv, :abbr].each do |p| result = lemma(form, p) return result unless result == form end return form end each_lemma(form, pos) do |x| return x end form end def each_lemma(form, pos) if lemma = @exceptions[pos][form] lemma.each { |x| yield x } end if pos == :noun && form.endwith('ful') each_lemma(form[0, form.length-3], pos) do |x| yield x + 'ful' end else each_substitutions(form, pos) do|x| yield x end end end # Print object only on init def inspect "#{self}" end private # ファイルから見出し語を取り出す前処理? def open_file(*args) # *argsは可変長引数 # args[0]がIOクラスかStringIOクラスなら、args[0]を返す if args[0].is_a? IO or args[0].is_a? StringIO yield args[0] else File.open(*args) do |io| yield io end end end def each_substitutions(form, pos) if lemma = @wordlists[pos][form] yield lemma end MORPHOLOGICAL_SUBSTITUTIONS[pos].each do |entry| # entryが展開されて、oldとnewに代入される old, new = *entry # formがoldで終わっている場合 if form.endwith(old) each_substitutions(form[0, form.length - old.length] + new, pos) do |x| yield x end end end end def str_to_pos(str) case str when "n", "noun" return :noun when "v", "verb" return :noun when "a", "j", "adjective", "adj" return :adj when "r", "adverb", "adv" return :adv when "b", "abbrev", "abbr", "abr" return :abbr else return :unknown end end end ##################辞書を利用するための初期化############################## # インスタンスの生成時に実行される # オプショナル変数。dictに値を渡さない場合はnilになる。 def initialize(dict = nil) @wordlists = {} @exceptions = {} # インスタンス変数 # スコープ:クラス内で全メソッドで共通して使用することが出来る。 # クラスから作成されるオブジェクト毎に固有のもの。 MORPHOLOGICAL_SUBSTITUTIONS.keys.each do |x| @wordlists[x] = {} @exceptions[x] = {} end # 実行後、@wordlistsと@exceptionsは次のデータになる # {:noun=>{}, :verb=>{}, :adj=>{}, :adv=>{}, :abbr=>{}, :unknown=>{}} WN_FILES.each_pair do |pos, pair| load_wordnet_files(pos, pair[0], pair[1]) end if dict [dict].flatten.each do |d| load_provided_dict(d) end end end end
index.品詞
の辞書は、各行の見出しだけを読み込んでいるとasmさんから助言をうけました。
lemmaの動作についてまとめ:
- 辞書の作成
・@wordlistsと@exceptions、2つのハッシュを辞書として作成。
・それぞれ、次のような構造。これらにデータを登録する。
{:noun=>{}, :verb=>{}, :adj=>{}, :adv=>{}, :abbr=>{}, :unknown=>{}}
・wordlistsに見出し語を登録する目的は、品詞を特定せずにlemmaを呼び出した際、どの品詞に属しているかを特定するため。
- lemmaメソッドの呼び出し
<品詞を特定する場合>
・@exceptionsに単語があれば、そのデータを元に原形を返す
・なければ、morphological substitution (形態論の置き換え)のルールにしたがって置き換える。
<品詞を特定しない場合>
・動詞->名詞->形容詞->副詞の順で[:verb, :noun, :adj, :adv, :abbr]、見出し語から品詞を特定する。
・@exceptionsに単語があれば、そこから原形を返す
・なければ、morphological substitution (形態論の置き換え)のルールにしたがって置き換える。
コードの解読を難しくしていた要因:
・辞書データを作成するためのメソッドが多かったこと
(NLTKの辞書を再利用していたため)
回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2019/04/25 07:31
2019/04/25 11:55
2019/04/25 11:59
2019/04/30 00:15
2019/05/02 00:37