質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

Q&A

解決済

1回答

1111閲覧

Ruby製ライブラリlemmatizerのコード解読

t-cool

総合スコア71

Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

0グッド

0クリップ

投稿2019/04/24 12:55

編集2019/05/01 00:58

Ruby製の英単語の原形を探すライブラリlemmatizerのソースコードを読んでいます。

レポジトリーはこちらです。

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の動作についてまとめ:

  1. 辞書の作成

  ・@wordlistsと@exceptions、2つのハッシュを辞書として作成。
・それぞれ、次のような構造。これらにデータを登録する。
{:noun=>{}, :verb=>{}, :adj=>{}, :adv=>{}, :abbr=>{}, :unknown=>{}}
・wordlistsに見出し語を登録する目的は、品詞を特定せずにlemmaを呼び出した際、どの品詞に属しているかを特定するため。

  1. lemmaメソッドの呼び出し

  <品詞を特定する場合>
・@exceptionsに単語があれば、そのデータを元に原形を返す
・なければ、morphological substitution (形態論の置き換え)のルールにしたがって置き換える。
<品詞を特定しない場合>
・動詞->名詞->形容詞->副詞の順で[:verb, :noun, :adj, :adv, :abbr]、見出し語から品詞を特定する。
・@exceptionsに単語があれば、そこから原形を返す
・なければ、morphological substitution (形態論の置き換え)のルールにしたがって置き換える。

コードの解読を難しくしていた要因:
・辞書データを作成するためのメソッドが多かったこと
(NLTKの辞書を再利用していたため)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

index.品詞の辞書ファイルについては行頭の単語以外を使っていません。


追記:

?? モジュールで self.new を定義すると、モジュールに対してnewが呼べる。

?? lemmatizer/lib/lemmatizer.rbで、モジュール読み込み時に、Lemmatizer.new(dict)を実行?

?? このコードは何のため?

module Lemmatizer

おそらく

ruby

1lem = Lemmatizer::Lemmatizer.new

と書くのがめんどくさいから

ruby

1lem = Lemmatizer.new

と書けるようにしているだけ

投稿2019/04/24 22:45

編集2019/04/29 09:54
asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

asm

2019/04/25 07:31

モチベーションがわからんから期待する回答がわからん・・・ index: 変形しない単語 exc: 例外的な変形をする単語 あとは、規則的に変形をしているだけっぽいんだが
t-cool

2019/04/25 11:55

asmさん モチベーションとしては、このライブラリをCommon Lispで移植しようとしていまして、ソースコードを追っています。 https://github.com/t-cool/cl-lemma `index.品詞`の中に入っているものが活用しないものかと思ったんですが、中を見ると活用するものも多く含まれているので、どこが読めてないのか、一つずつメソッドの動作を追っています。自力でそれぞれのメソッドの動作を記述しながら、全体の動作を理解しようとしています。完成後、ご助言をいただけますと、ありがたいです。
asm

2019/04/25 11:59

indexを読んだあとに、excを読んでるので もしかしたら、index内で活用するものをexcで上書きしてるかもしれませんね
t-cool

2019/04/30 00:15

だいぶとわかってきました。最後まで読み解けた後に、ベストアンサーにしますので、少々お待ちください。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問