From 70842ecfb7e84abed751eef044da4ba8da722cd0 Mon Sep 17 00:00:00 2001 From: "alexander.a.kuznetsov" Date: Tue, 11 Aug 2009 16:16:56 +0000 Subject: [PATCH] working on morh git-svn-id: https://russianmorphology.googlecode.com/svn/trunk@38 d817d54c-26ab-11de-abc9-2f7d1455ff7a --- .../russian/morphology/HeuristicBuilder.java | 76 +++------ .../RussianSuffixDecoderEncoder.java | 6 +- .../lucene/russian/morphology/Test.java | 2 +- .../morphology/analayzer/SuffixHeuristic.java | 2 +- .../morphology/dictonary/GrammaReader.java | 14 +- .../morphology/heuristic/Heuristic.java | 8 +- .../heuristic/HeuristicBySuffixLegth.java | 76 ++++++++- .../heuristic/SimpleSuffixHeuristic.java | 85 ++++++++++ .../heuristic/StatiticsCollectors.java | 30 ++-- .../morphology/heuristic/SuffixCounter.java | 16 +- .../morphology/heuristic/SuffixHeuristic.java | 95 ++--------- .../heuristic/SuffixHeuristicMerger.java | 13 -- .../morphology/heuristic/SuffixTypes.java | 8 + .../morphology/informations/GrammaInfo.java | 16 ++ .../informations/NormalSuffixCollection.java | 16 ++ .../lucene/russian/morphology/SpeedTest.java | 45 +++++ .../apache/lucene/russian/morphology/text.txt | 160 ++++++++++++++++++ 17 files changed, 468 insertions(+), 200 deletions(-) create mode 100644 src/main/java/org/apache/lucene/russian/morphology/heuristic/SimpleSuffixHeuristic.java delete mode 100644 src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristicMerger.java create mode 100644 src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixTypes.java create mode 100644 src/main/java/org/apache/lucene/russian/morphology/informations/GrammaInfo.java create mode 100644 src/main/java/org/apache/lucene/russian/morphology/informations/NormalSuffixCollection.java create mode 100644 src/test/java/org/apache/lucene/russian/morphology/SpeedTest.java create mode 100644 src/test/resources/org/apache/lucene/russian/morphology/text.txt diff --git a/src/main/java/org/apache/lucene/russian/morphology/HeuristicBuilder.java b/src/main/java/org/apache/lucene/russian/morphology/HeuristicBuilder.java index bbe8e32..542a5a9 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/HeuristicBuilder.java +++ b/src/main/java/org/apache/lucene/russian/morphology/HeuristicBuilder.java @@ -16,20 +16,15 @@ package org.apache.lucene.russian.morphology; -import org.apache.lucene.russian.morphology.dictonary.DictonaryReader; -import org.apache.lucene.russian.morphology.dictonary.FrequentyReader; -import org.apache.lucene.russian.morphology.dictonary.GrammaReader; -import org.apache.lucene.russian.morphology.dictonary.IgnoredFormReader; +import org.apache.lucene.russian.morphology.dictonary.*; import org.apache.lucene.russian.morphology.heuristic.HeuristicBySuffixLegth; import org.apache.lucene.russian.morphology.heuristic.StatiticsCollectors; import org.apache.lucene.russian.morphology.heuristic.SuffixCounter; -import org.apache.lucene.russian.morphology.heuristic.SuffixHeuristic; +import org.apache.lucene.russian.morphology.heuristic.SimpleSuffixHeuristic; import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Set; -import java.util.TreeMap; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; public class HeuristicBuilder { @@ -57,47 +52,28 @@ public class HeuristicBuilder { heuristic.addHeuristic(((SuffixCounter) objects[i]).getSuffixHeuristic()); } - TreeMap map = new TreeMap(); + final Map> map = heuristic.getUnkowns(); - int ct = 0; - for (Set s : heuristic.getHeuristics().values()) { - Integer d = map.get(s.size()); - map.put(s.size(), 1 + (d == null ? 0 : d)); - if (s.size() == 1) { - ct++; - continue; - } - SuffixHeuristic heuristic1 = s.iterator().next(); - Integer sufixSize = heuristic1.getActualSuffixLength(); - String normalSuffix = heuristic1.getNormalFromSuffix(); - if (heuristic1.getFormSuffix().length() < 6) { - ct++; - continue; - } - Boolean flag = true; - if (sufixSize > 3) continue; - for (SuffixHeuristic sh : s) { - flag = flag && (sufixSize.equals(sh.getActualSuffixLength())) - && (normalSuffix.equals(sh.getNormalFromSuffix())); - } - if (flag) { - System.out.println(s); - ct++; - } - //HashSet integers = new HashSet(); -// for(SuffixHeuristic sh:s){ -// integers.add(sh.getMorphInfoCode()); -// } -// if(s.size() == integers.size()){ -// ct++; -// }else{ -// if(s.size() == 2) System.out.println(s); -// } - } - System.out.println(objects.length); - System.out.println(heuristic.getHeuristics().size()); - System.out.println(ct); - System.out.println(map); - //heuristic.writeToFile("russianSuffixesHeuristic.txt"); + //final RussianSuffixDecoderEncoder decoderEncoder = new RussianSuffixDecoderEncoder(6); + final AtomicLong c = new AtomicLong(0L); + final AtomicLong all = new AtomicLong(0L); + dictonaryReader.proccess( + new WordProccessor(){ + public void proccess(WordCard wordCard) throws IOException { + for (FlexiaModel fm : wordCard.getWordsFroms()) { + String form = fm.create(wordCard.getBase()); + int startSymbol = form.length() > RussianSuffixDecoderEncoder.suffixLength ? form.length() - RussianSuffixDecoderEncoder.suffixLength : 0; + String formSuffix = form.substring(startSymbol); + Long aLong = RussianSuffixDecoderEncoder.encode(formSuffix); + all.incrementAndGet(); + if(map.containsKey(aLong)) c.incrementAndGet(); + } + } + } + ); + + + System.out.println("Ankown words " + all.longValue()); + System.out.println("Ankown words " + c.longValue()); } } diff --git a/src/main/java/org/apache/lucene/russian/morphology/RussianSuffixDecoderEncoder.java b/src/main/java/org/apache/lucene/russian/morphology/RussianSuffixDecoderEncoder.java index 3802050..38d1d2b 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/RussianSuffixDecoderEncoder.java +++ b/src/main/java/org/apache/lucene/russian/morphology/RussianSuffixDecoderEncoder.java @@ -24,13 +24,17 @@ package org.apache.lucene.russian.morphology; */ public class RussianSuffixDecoderEncoder { public static final int RUSSIAN_SMALL_LETTER_OFFSET = 1071; - public static final int SUFFIX_LENGTH = 6; + static public int suffixLength = 6; public static final int EE_CHAR = 34; public static final int E_CHAR = 6; public static final int DASH_CHAR = 45; public static final int DASH_CODE = 33; + public RussianSuffixDecoderEncoder(int suffixLength) { + RussianSuffixDecoderEncoder.suffixLength = suffixLength; + } + static public Long encode(String string) { if (string.length() > 12) throw new SuffixToLongException("Suffix length should not be greater then " + 12); long result = 0L; diff --git a/src/main/java/org/apache/lucene/russian/morphology/Test.java b/src/main/java/org/apache/lucene/russian/morphology/Test.java index 3badcdd..1313c13 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/Test.java +++ b/src/main/java/org/apache/lucene/russian/morphology/Test.java @@ -8,6 +8,6 @@ import java.io.IOException; public class Test { public static void main(String[] args) throws IOException { GrammaReader grammaReader = new GrammaReader("dictonary/Dicts/Morph/rgramtab.tab"); - System.out.println(grammaReader.getInversIndex().size()); + //System.out.println(grammaReader.getInversIndex().size()); } } diff --git a/src/main/java/org/apache/lucene/russian/morphology/analayzer/SuffixHeuristic.java b/src/main/java/org/apache/lucene/russian/morphology/analayzer/SuffixHeuristic.java index a8db9e2..4100c35 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/analayzer/SuffixHeuristic.java +++ b/src/main/java/org/apache/lucene/russian/morphology/analayzer/SuffixHeuristic.java @@ -58,7 +58,7 @@ public class SuffixHeuristic { } public String getCanonicalForm(String form) { - int startSymbol = form.length() > RussianSuffixDecoderEncoder.SUFFIX_LENGTH ? form.length() - RussianSuffixDecoderEncoder.SUFFIX_LENGTH : 0; + int startSymbol = form.length() > RussianSuffixDecoderEncoder.suffixLength ? form.length() - RussianSuffixDecoderEncoder.suffixLength : 0; String suffixS = form.substring(startSymbol); if (!chechSuffix(suffixS)) return form; diff --git a/src/main/java/org/apache/lucene/russian/morphology/dictonary/GrammaReader.java b/src/main/java/org/apache/lucene/russian/morphology/dictonary/GrammaReader.java index 038a737..8ace76d 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/dictonary/GrammaReader.java +++ b/src/main/java/org/apache/lucene/russian/morphology/dictonary/GrammaReader.java @@ -6,12 +6,14 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; +import java.util.List; +import java.util.ArrayList; //todo spleet this class on two. public class GrammaReader { private String fileName; private String fileEncoding = "windows-1251"; - private Map grammaInfo = new HashMap(); + private List grammaInfo = new ArrayList(); private Map inversIndex = new HashMap(); public GrammaReader(String fileName) throws IOException { @@ -34,21 +36,17 @@ public class GrammaReader { String[] strings = line.split(" ", 2); Integer i = grammaInfo.size(); inversIndex.put(strings[0], i); - grammaInfo.put(i, strings[1]); + grammaInfo.add(i, strings[1]); } line = bufferedReader.readLine(); } } - public Map getGrammaInfo() { + public List getGrammaInfo() { return grammaInfo; } - public void setGrammaInfo(Map grammaInfo) { - this.grammaInfo = grammaInfo; - } - - public Map getInversIndex() { + public Map getGrammInversIndex() { return inversIndex; } diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/Heuristic.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/Heuristic.java index 0463511..9ed5d82 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/Heuristic.java +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/Heuristic.java @@ -28,16 +28,16 @@ import java.util.TreeMap; public class Heuristic { private TreeMap encodedSuffixesPairs = new TreeMap(); - public void addHeuristic(SuffixHeuristic suffixHeuristic) { -// Long suffix = RussianSuffixDecoderEncoder.encode(suffixHeuristic.getFormSuffix()); + public void addHeuristic(SimpleSuffixHeuristic simpleSuffixHeuristic) { +// Long suffix = RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getFormSuffix()); // Long longs = encodedSuffixesPairs.get(suffix); // if (longs == null) { -// encodedSuffixesPairs.put(suffix, RussianSuffixDecoderEncoder.encode(suffixHeuristic.getNormalSuffix())); +// encodedSuffixesPairs.put(suffix, RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getNormalSuffix())); // } } public String getNormalForm(String form) { - int startSymbol = form.length() > RussianSuffixDecoderEncoder.SUFFIX_LENGTH ? form.length() - RussianSuffixDecoderEncoder.SUFFIX_LENGTH : 0; + int startSymbol = form.length() > RussianSuffixDecoderEncoder.suffixLength ? form.length() - RussianSuffixDecoderEncoder.suffixLength : 0; Long suffix = RussianSuffixDecoderEncoder.encode(form.substring(startSymbol)); Long normalSuffix = encodedSuffixesPairs.get(suffix); diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/HeuristicBySuffixLegth.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/HeuristicBySuffixLegth.java index d6d736e..4432e3b 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/HeuristicBySuffixLegth.java +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/HeuristicBySuffixLegth.java @@ -9,19 +9,77 @@ import java.util.Set; public class HeuristicBySuffixLegth { - private Map> heuristics = new HashMap>(); + private Map> heuristics = new HashMap>(); - public void addHeuristic(SuffixHeuristic suffixHeuristic) { - Long suffix = RussianSuffixDecoderEncoder.encode(suffixHeuristic.getFormSuffix()); - Set suffixHeuristics = heuristics.get(suffix); - if (suffixHeuristics == null) { - suffixHeuristics = new HashSet(); - heuristics.put(suffix, suffixHeuristics); + public void addHeuristic(SimpleSuffixHeuristic simpleSuffixHeuristic) { + Long suffix = RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getFormSuffix()); + Set simpleSuffixHeuristics = heuristics.get(suffix); + if (simpleSuffixHeuristics == null) { + simpleSuffixHeuristics = new HashSet(); + heuristics.put(suffix, simpleSuffixHeuristics); } - suffixHeuristics.add(suffixHeuristic); + simpleSuffixHeuristics.add(simpleSuffixHeuristic); } - public Map> getHeuristics() { + public Map> getHeuristics() { return heuristics; } + + public Map getSingleSuffixes(){ + HashMap result = new HashMap(); + for(Long st:heuristics.keySet()){ + if(heuristics.get(st).size() == 1){ + result.put(st,heuristics.get(st).iterator().next()); + } + } + return result; + } + + + public Map> getWordWithMorphology(){ + HashMap> result = new HashMap>(); + for(Long st:heuristics.keySet()){ + if(heuristics.get(st).size() == 1) continue; + if(checkSetOnSuffix(heuristics.get(st))) { + result.put(st,heuristics.get(st)); + } + } + return result; + } + + public Map> getOnonyms(){ + HashMap> result = new HashMap>(); + for(Long st:heuristics.keySet()){ + if(heuristics.get(st).size() == 1) continue; + if(checkSetOnSuffix(heuristics.get(st))) continue; + if(heuristics.get(st).iterator().next().getFormSuffix().length() < 6){ + result.put(st,heuristics.get(st)); + } + } + return result; + } + + public Map> getUnkowns(){ + HashMap> result = new HashMap>(); + for(Long st:heuristics.keySet()){ + if(heuristics.get(st).size() == 1) continue; + if(checkSetOnSuffix(heuristics.get(st))) continue; + if(heuristics.get(st).iterator().next().getFormSuffix().length() >= 6){ + result.put(st,heuristics.get(st)); + } + } + return result; + } + + private Boolean checkSetOnSuffix(Set sshs) { + SimpleSuffixHeuristic heuristic = sshs.iterator().next(); + String normalSuffix = heuristic.getFormSuffix(); + Integer suffixLenght = heuristic.getActualSuffixLength(); + Boolean result = true; + for(SimpleSuffixHeuristic ssh:sshs){ + result = result && ssh.getActualSuffixLength().equals(suffixLenght) && ssh.getNormalSuffix().endsWith(normalSuffix); + } + return result; + } + } diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SimpleSuffixHeuristic.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SimpleSuffixHeuristic.java new file mode 100644 index 0000000..95c6b70 --- /dev/null +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SimpleSuffixHeuristic.java @@ -0,0 +1,85 @@ +/** + * Copyright 2009 Alexander Kuznetsov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.lucene.russian.morphology.heuristic; + +/** + * Represent evristic that assume that + * canonical from of word is defined by word suffix. + * It contains to suffixes from given position of + * canonical word form and for form. + */ +public class SimpleSuffixHeuristic { + private String formSuffix; + private Integer actualSuffixLength; + private String normalSuffix; + private String morphInfoCode; + + public SimpleSuffixHeuristic(String formSuffix, Integer actualSuffixLength, String normalSuffix, String morphInfoCode) { + this.formSuffix = formSuffix; + this.actualSuffixLength = actualSuffixLength; + this.normalSuffix = normalSuffix; + this.morphInfoCode = morphInfoCode; + } + + public String getFormSuffix() { + return formSuffix; + } + + public Integer getActualSuffixLength() { + return actualSuffixLength; + } + + public String getNormalSuffix() { + return normalSuffix; + } + + public String getMorphInfoCode() { + return morphInfoCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimpleSuffixHeuristic that = (SimpleSuffixHeuristic) o; + + if (actualSuffixLength != null ? !actualSuffixLength.equals(that.actualSuffixLength) : that.actualSuffixLength != null) + return false; + if (formSuffix != null ? !formSuffix.equals(that.formSuffix) : that.formSuffix != null) return false; + if (morphInfoCode != null ? !morphInfoCode.equals(that.morphInfoCode) : that.morphInfoCode != null) + return false; + if (normalSuffix != null ? !normalSuffix.equals(that.normalSuffix) : that.normalSuffix != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = formSuffix != null ? formSuffix.hashCode() : 0; + result = 31 * result + (actualSuffixLength != null ? actualSuffixLength.hashCode() : 0); + result = 31 * result + (normalSuffix != null ? normalSuffix.hashCode() : 0); + result = 31 * result + (morphInfoCode != null ? morphInfoCode.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return formSuffix + " " + actualSuffixLength + " " + normalSuffix + " " + morphInfoCode; + } +} diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/StatiticsCollectors.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/StatiticsCollectors.java index db9782b..b9dc025 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/StatiticsCollectors.java +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/StatiticsCollectors.java @@ -27,7 +27,7 @@ import java.util.Map; public class StatiticsCollectors implements WordProccessor { - Map statititics = new HashMap(); + Map statititics = new HashMap(); private Map wordsFreq; private GrammaReader grammaInfo; @@ -40,12 +40,12 @@ public class StatiticsCollectors implements WordProccessor { public void proccess(WordCard wordCard) { for (FlexiaModel fm : wordCard.getWordsFroms()) { - SuffixHeuristic suffixHeuristic = createEvristic(wordCard.getCanonicalFrom(), wordCard.getCanonicalSuffix(), fm); - if (suffixHeuristic == null) continue; - SuffixCounter suffixCounter = statititics.get(suffixHeuristic); + SimpleSuffixHeuristic simpleSuffixHeuristic = createEvristic(wordCard.getBase(), wordCard.getCanonicalSuffix(), fm); + if (simpleSuffixHeuristic == null) continue; + SuffixCounter suffixCounter = statititics.get(simpleSuffixHeuristic); if (suffixCounter == null) { - suffixCounter = new SuffixCounter(suffixHeuristic); - statititics.put(suffixHeuristic, suffixCounter); + suffixCounter = new SuffixCounter(simpleSuffixHeuristic); + statititics.put(simpleSuffixHeuristic, suffixCounter); } Double freq = wordsFreq.get(wordCard.getCanonicalFrom()); if (freq != null) { @@ -57,27 +57,17 @@ public class StatiticsCollectors implements WordProccessor { } } - public Map getStatititics() { + public Map getStatititics() { return statititics; } - private SuffixHeuristic createEvristic(String wordBase, String canonicalSuffix, FlexiaModel fm) { + private SimpleSuffixHeuristic createEvristic(String wordBase, String canonicalSuffix, FlexiaModel fm) { String form = fm.create(wordBase); - int startSymbol = form.length() > RussianSuffixDecoderEncoder.SUFFIX_LENGTH ? form.length() - RussianSuffixDecoderEncoder.SUFFIX_LENGTH : 0; + int startSymbol = form.length() > RussianSuffixDecoderEncoder.suffixLength ? form.length() - RussianSuffixDecoderEncoder.suffixLength : 0; String formSuffix = form.substring(startSymbol); String actualSuffix = fm.getSuffix(); Integer actualSuffixLengh = actualSuffix.length(); -// if (word.length() < startSymbol) { -// ignoredCount++; -// return null; -// } -// String wordSuffix = word.length() > startSymbol ? word.substring(startSymbol) : ""; -// if (wordSuffix.length() > 12) { -// System.out.println(word + " " + form); -// return null; -// } -// return new SuffixHeuristic(formSuffix, wordSuffix); - return new SuffixHeuristic(formSuffix, actualSuffixLengh, canonicalSuffix, fm.getCode()); + return new SimpleSuffixHeuristic(formSuffix, actualSuffixLengh, canonicalSuffix, fm.getCode()); } diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixCounter.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixCounter.java index d66f9b5..2db5a07 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixCounter.java +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixCounter.java @@ -21,11 +21,11 @@ package org.apache.lucene.russian.morphology.heuristic; * in dictionary. */ public class SuffixCounter implements Comparable { - private SuffixHeuristic suffixHeuristic; + private SimpleSuffixHeuristic simpleSuffixHeuristic; private Double amnout = 0.0; - public SuffixCounter(SuffixHeuristic suffixHeuristic) { - this.suffixHeuristic = suffixHeuristic; + public SuffixCounter(SimpleSuffixHeuristic simpleSuffixHeuristic) { + this.simpleSuffixHeuristic = simpleSuffixHeuristic; } public void incrementAmount() { @@ -36,12 +36,12 @@ public class SuffixCounter implements Comparable { amnout += wordFreq; } - public SuffixHeuristic getSuffixHeuristic() { - return suffixHeuristic; + public SimpleSuffixHeuristic getSuffixHeuristic() { + return simpleSuffixHeuristic; } - public void setSuffixEvristic(SuffixHeuristic suffixHeuristic) { - this.suffixHeuristic = suffixHeuristic; + public void setSuffixEvristic(SimpleSuffixHeuristic simpleSuffixHeuristic) { + this.simpleSuffixHeuristic = simpleSuffixHeuristic; } public Double getAmnout() { @@ -59,6 +59,6 @@ public class SuffixCounter implements Comparable { @Override public String toString() { - return "" + amnout + " " + suffixHeuristic.toString(); + return "" + amnout + " " + simpleSuffixHeuristic.toString(); } } diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristic.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristic.java index 8dfadc8..0bdf6f8 100644 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristic.java +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristic.java @@ -1,85 +1,10 @@ -/** - * Copyright 2009 Alexander Kuznetsov - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.lucene.russian.morphology.heuristic; - -/** - * Represent evristic that assume that - * canonical from of word is defined by word suffix. - * It contains to suffixes from given position of - * canonical word form and for form. - */ -public class SuffixHeuristic { - private String formSuffix; - private Integer actualSuffixLength; - private String normalFromSuffix; - private String morphInfoCode; - - public SuffixHeuristic(String formSuffix, Integer actualSuffixLength, String normalFromSuffix, String morphInfoCode) { - this.formSuffix = formSuffix; - this.actualSuffixLength = actualSuffixLength; - this.normalFromSuffix = normalFromSuffix; - this.morphInfoCode = morphInfoCode; - } - - public String getFormSuffix() { - return formSuffix; - } - - public Integer getActualSuffixLength() { - return actualSuffixLength; - } - - public String getNormalFromSuffix() { - return normalFromSuffix; - } - - public String getMorphInfoCode() { - return morphInfoCode; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SuffixHeuristic that = (SuffixHeuristic) o; - - if (actualSuffixLength != null ? !actualSuffixLength.equals(that.actualSuffixLength) : that.actualSuffixLength != null) - return false; - if (formSuffix != null ? !formSuffix.equals(that.formSuffix) : that.formSuffix != null) return false; - if (morphInfoCode != null ? !morphInfoCode.equals(that.morphInfoCode) : that.morphInfoCode != null) - return false; - if (normalFromSuffix != null ? !normalFromSuffix.equals(that.normalFromSuffix) : that.normalFromSuffix != null) - return false; - - return true; - } - - @Override - public int hashCode() { - int result = formSuffix != null ? formSuffix.hashCode() : 0; - result = 31 * result + (actualSuffixLength != null ? actualSuffixLength.hashCode() : 0); - result = 31 * result + (normalFromSuffix != null ? normalFromSuffix.hashCode() : 0); - result = 31 * result + (morphInfoCode != null ? morphInfoCode.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return formSuffix + " " + actualSuffixLength + " " + normalFromSuffix + " " + morphInfoCode; - } -} +package org.apache.lucene.russian.morphology.heuristic; + + +public class SuffixHeuristic { + private SuffixTypes suffixType; + private Byte suffixLengh; + private Short indexOfWordTransorm; + private Short indexOfMothInfo; +} + diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristicMerger.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristicMerger.java deleted file mode 100644 index cbaac4a..0000000 --- a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixHeuristicMerger.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.apache.lucene.russian.morphology.heuristic; - - -public class SuffixHeuristicMerger { - - public SuffixHeuristic merge(SuffixHeuristic one, SuffixHeuristic two) { - if (!one.getMorphInfoCode().equals(two.getMorphInfoCode())) - return null; - SuffixHeuristic min = one.getActualSuffixLength() > two.getActualSuffixLength() ? two : one; - - return null; - } -} diff --git a/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixTypes.java b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixTypes.java new file mode 100644 index 0000000..3d4a33d --- /dev/null +++ b/src/main/java/org/apache/lucene/russian/morphology/heuristic/SuffixTypes.java @@ -0,0 +1,8 @@ +package org.apache.lucene.russian.morphology.heuristic; + + +public enum SuffixTypes { + SINGLE, + DIFFIRENT_MORPH, + ONONIMS +} diff --git a/src/main/java/org/apache/lucene/russian/morphology/informations/GrammaInfo.java b/src/main/java/org/apache/lucene/russian/morphology/informations/GrammaInfo.java new file mode 100644 index 0000000..cd37b23 --- /dev/null +++ b/src/main/java/org/apache/lucene/russian/morphology/informations/GrammaInfo.java @@ -0,0 +1,16 @@ +package org.apache.lucene.russian.morphology.informations; + +import java.io.Serializable; + + +public class GrammaInfo implements Serializable{ + private String[] grammaInfo; + + public GrammaInfo(String[] grammaInfo) { + this.grammaInfo = grammaInfo; + } + + public String getInfo(Integer index){ + return grammaInfo[index]; + } +} diff --git a/src/main/java/org/apache/lucene/russian/morphology/informations/NormalSuffixCollection.java b/src/main/java/org/apache/lucene/russian/morphology/informations/NormalSuffixCollection.java new file mode 100644 index 0000000..efdebd7 --- /dev/null +++ b/src/main/java/org/apache/lucene/russian/morphology/informations/NormalSuffixCollection.java @@ -0,0 +1,16 @@ +package org.apache.lucene.russian.morphology.informations; + +import java.io.Serializable; + + +public class NormalSuffixCollection implements Serializable{ + private String[] normalSuffixes; + + public NormalSuffixCollection(String[] normalSuffixes) { + this.normalSuffixes = normalSuffixes; + } + + public String getSuffix(Integer index){ + return normalSuffixes[index]; + } +} diff --git a/src/test/java/org/apache/lucene/russian/morphology/SpeedTest.java b/src/test/java/org/apache/lucene/russian/morphology/SpeedTest.java new file mode 100644 index 0000000..35ea625 --- /dev/null +++ b/src/test/java/org/apache/lucene/russian/morphology/SpeedTest.java @@ -0,0 +1,45 @@ +package org.apache.lucene.russian.morphology; + +import org.junit.Test; +import static org.junit.Assert.assertThat; +import org.apache.lucene.russian.morphology.analayzer.RussianMorphlogyAnalayzer; +import org.apache.lucene.analysis.Token; +import org.apache.lucene.analysis.TokenStream; +import static org.hamcrest.core.IsEqual.equalTo; + +import java.io.InputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; + + +public class SpeedTest { + + @Test + public void getTestOfSpeed() throws IOException { + Long startTime = System.currentTimeMillis(); + RussianMorphlogyAnalayzer morphlogyAnalayzer = new RussianMorphlogyAnalayzer(); + System.out.println("To build analayzer take " + (System.currentTimeMillis() - startTime) + " ms."); + InputStream stream = this.getClass().getResourceAsStream("/org/apache/lucene/russian/morphology/text.txt"); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + + + final Token reusableToken = new Token(); + + Token nextToken; + + + startTime = System.currentTimeMillis(); + Integer count = 0; + TokenStream in = morphlogyAnalayzer.tokenStream(null, reader); + for (; ;) { + nextToken = in.next(reusableToken); + count++; + if (nextToken == null) { + break; + } + + } + System.out.println("It takes " + (System.currentTimeMillis() - startTime) + " ms. To proccess " + count + " words." ); + } +} diff --git a/src/test/resources/org/apache/lucene/russian/morphology/text.txt b/src/test/resources/org/apache/lucene/russian/morphology/text.txt new file mode 100644 index 0000000..449a01e --- /dev/null +++ b/src/test/resources/org/apache/lucene/russian/morphology/text.txt @@ -0,0 +1,160 @@ +Морфологический анализатор для русского языка — это что-то заумное? Программа, которая приводит слово к начальной форме, определяет падеж, находит словоформы — непонятно, как и подступиться? А на самом деле все не так и сложно. В статье — как я писал аналог mystem, lemmatizer и phpmorphy на Python, и что из этого получилось. + +Сразу скажу, получилась библиотека для морфологического анализа на Python, которую Вы можете использовать и дорабатывать согласно лицензии MIT. + +Первый вопрос — зачем все это? + +Потихоньку делаю один хобби-проект, назрело решение писать его на Python. А для проекта был жизненно необходим более-менее качественный морфологический анализатор. Вариант с mystem не подошел из-за лицензии и отсутствием возможности поковыряться, lemmatizer и phpmorphy меня слегка напрягали своими словарями не в юникоде, а приличного аналога на python я почему-то не нашел. Вообщем причин много, все, если честно, не очень серьезные, на самом деле просто захотелось. Решил я, в итоге, изобрести велосипед и написать свою реализацию. + +За основу взял наработки с сайта aot.ru. Словари (LGPL) для русского и английского, а также идеи — оттуда. Там были описаны и конкретные алгоритмы реализации, но в терминах теории автоматов. Я знаком с теорией автоматов (даже диплом по формальным грамматикам защитил), но мне показалось, что тут можно прекрасно обойтись без нее. Поэтому реализация — независимая, что имеет как плюсы, так и минусы. + +Для начала, библиотека должна, как минимум, уметь: +1. Приводить слово к нормальной форме (например, в ед.ч., И.п. для существительных) — для слова «ЛЮДЕЙ» она должна вернуть «ЧЕЛОВЕК» +2. Определять форму слова (или формы). Например, для слова «ЛЮДЕЙ» она должна как-то дать знать, что это существительное, во множественном числе, может оказаться в родительном или винительном падежах. + +Разбираемся со словарями + +Словари с сайта aot.ru содержат следующую информацию: +1. парадигмы слов и конкретные правила образования; +2. ударения; +3. пользовательские сессии; +4. набор префиксов (продуктивных приставок); +5. леммы (незменяемые части слова, основы); +и еще есть 6. грамматическая информация — в отдельном файле. + +Из этого всего нам интересны правила образования слов, префиксы, леммы и грамматическая информация. + +Все слова образуются по одному принципу: +[префикс]+[приставка]+[основа]+[окончание] + +Префиксы — это всякие «мега», «супер» и т.д. Набор префиксов хранится просто списком. + +Приставки — имеются в виду приставки, присущие грамматической форме, но не присущие неизменяемой части слова («по»,«наи»). Например, «наи» в слове «наикрасивейший», т.к. без превосходной степени будет «красивый». + +Правила образования слов — это то, что надо приписать спереди и сзади основы, чтобы получить какую-то форму. В словаре хранятся пары «приставка — окончание», + «номер» записи о грамматической информации (которая хранится отдельно). + +Правила образования слов объединены в парадигмы. Например, для какого-нибудь класса существительных может быть описано, как слово выглядит во всех падежах и родах. Зная, что существительное принадлежит к этому классу, мы сможем правильно получить любую его форму. Такой класс — это и есть парадигма. Первой в парадигме всегда идет нормальная форма слова (если честно, вывод эмпирический)) + +Леммы — это неизменяемые части слов. В словаре хранится информация о том, какой лемме соответствуют какие парадигмы (какой набор правил для образования грамматических форм слова). Одной лемме может соответствовать несколько парадигм. + +Грамматическая информация — просто пары («номер» записи, грам. информация). «Номер» в кавычках, т.к. это 2 буквы, просто от балды, но все разные. + +Файл со словарем — обычный текстовый, для каждого раздела сначала указано число строк в нем, а потом идут строки, формат их описан, так что написать парсер труда не составило. + +Поняв структуру словаря, уже несложно написать первую версию морфологического анализатора. + +Пишем морфологический анализатор + +По сути, нам дано слово, и его надо найти среди всех разумных комбинаций вида +<префикс>+<приставка>+<лемма>+<окончание> и +<приставка>+<лемма>+<окончание> + +Дело упрощает то, что оказалось (как показала пара строчек на питоне), что «приставок» у нас в языке (да и в английском вроде тоже) всего 2. А префиксов в словаре — порядка 20 для русского языка. Поэтому искать можно среди комбинаций +<префикс>+<лемма>+<окончание>, объединив в уме список приставок и префиксов, а затем выполнив небольшую проверочку. + +С префиксом разберемся по-свойски: если слово начинается с одного из возможных префиксов, то мы его (префикс) отбрасываем и пытаемся морфологически анализировать остаток (рекурсивно), а потом просто припишем отброшенный префикс к полученным формам. + +В итоге получается, что задача сводится к поиску среди комбинаций +<лемма>+<окончание>, что еще лучше. Ищем подходящие леммы, потом смотрим, есть ли для них подходящие окончания*. + +Тут я решил сильно упростить себе жизнь, и задействовать стандартный питоновский ассоциативный массив, в который поместил все леммы. Получился словарь вида lemmas: {base -> [rule_id]}, т.е. ключ — это лемма, а значение — список номеров допустимых парадигм. А дальше поехали — сначала считаем, что лемма — это первая буква слова, потом, что это 2 первых буквы и т.д. По лемме пытаемся получить список парадигм. Если получили, то в каждой допустимой парадигме пробегаем по всем правилам и смотрим, получится ли наше слово, если правило применить. Получается — добавляем его в список найденных форм. + +*Еще был вариант — составить сразу словарь всех возможных слов вида <лемма>+<окончание>, получалось в итоге где-то миллионов 5 слов, не так и много, но вариант, вообщем, мне не очень понравился. + +Дописываем морфологический анализатор + +По сути — все готово, мы написали морфологический анализатор, за исключением некоторых деталей, которые заняли у меня 90% времени) + +Деталь первая +Если вспомнить пример, который был в начале, про «ЛЮДЕЙ» — «ЧЕЛОВЕК», то станет понятно, что есть слова, у которых неизменяемая часть отсутствует. И где тогда искать формы этих слов — непонятно. Пробовал искать среди всех окончаний (точно так же, как и среди лемм), ведь для таких слов и «ЛЮДЕЙ», и «ЧЕЛОВЕКУ» в словаре будут храниться как окончания. Для некоторых слов работало, для некоторых выдавало, кроме правильного результата, еще и кучу мусора. В итоге, после долгих экспериментов выяснилось, что есть в словаре такая хитрая магическая лемма "#", которая и соответствует всем пустым леммам. Ура, задача решена, ищем каждый раз еще и там. + +Деталь вторая +Хорошо бы иметь «предсказатель», который смог бы работать и со словами, которых нет в словаре. Это не только неизвестные науке редкие слова, но и просто описки, например. + +Тут есть 2 несложных, но вполне работающих подхода: +1. Если слова отличаются только тем, что к одному из них приписано что-то спереди, то, скорее всего, склоняться они будут однаково +2. Если 2 слова оканчиваются одинаково, то и склоняться они, скорее всего, будут одинаково. + +Первый подход — это угадывание префикса. Реализуется очень просто: пробуем считать сначала одну первую букву слова префиксом, потом 2 первых буквы и т.д. А то, что осталось, передаем морфологическому анализатору. Ну и делаем это только для не очень длинных префиксов и не очень коротких остатков. + +Второй (предсказание по концу слова) подход чуть сложнее в реализации (так-то сильно сложнее, если нужна хорошая реализация)) и «поумнее» в плане предсказаний. +Первая сложность связана с тем, что конец слова может состоять не только из окончания, но и из части леммы. Тут я тоже решил «срезать углы» и задействовал опять ассоциативный массив с предварительно подготовленными всеми возмоными окончаниями слов (до 5 букв). Не так и много их получилось, несколько сот тысяч. Ключ массива — конец слова, значение — список возможных правил. Дальше — все как при поиске подходящей леммы, только у слова берем не начало, а 1, 2, 3, 4, 5-буквенные концы, а вместо лемм у нас теперь новый монстромассив. +Вторая сложность — получается много заведомого мусора. Мусор этот отсекается, если учесть, что полученные слова могут быть только существительными, прилагательными, наречиями или глаголами. +Даже после этого у нас остается слишком много не-мусорных правил. Для определенности, для каждой части речи оставляем только самое распространенное правило. +По идее, если слово не было предсказано как существительное, хорошо бы добавить вариант с неизменяемым существительным в ед.ч. и.п., но это в TODO. + +Идеальный текст для проверки работы предсказателя — конечно же, «Сяпала Калуша по напушке», про то, как она там увазила бутявку и что из этого вышло: + + Сяпала Калуша по напушке и увазила бутявку. И волит: + — Калушата, калушаточки! Бутявка! + Калушата присяпали и бутявку стрямкали. И подудонились. + А Калуша волит: + — Оее, оее! Бутявка-то некузявая! + Калушата бутявку вычучили. + Бутявка вздребезнулась, сопритюкнулась и усяпала с напушки. + А Калуша волит: + — Бутявок не трямкают. Бутявки дюбые и зюмо-зюмо некузявые. От бутявок дудонятся. + А бутявка волит за напушкой: + — Калушата подудонились! Калушата подудонились! Зюмо некузявые! Пуськи бятые! + + +Из текста предсказатель не справился с именем собственным Калуша, с «Калушата»(они стали мужиками «Калуш» и «Калушат»), с междометием «Оее», загадочным «зюмо-зюмо», и вместо «Пуська» опять выдал мужика «Пусек», остальное все, на мой взгляд, определил правильно. Вообщем есть куда стремиться, но уже неплохо. + +О ужас, «хабрахабр» предсказывается тоже никак. А вот «хабрахабра» — уже понимает, что «хабрахабр». + +Тут можно было бы, в принципе, подвести итог, если бы компьютеры работали мгновенно. Но это не так, поэтому есть + +Деталь №3: ресурсы процессора и оперативной памяти +Скорость работы первого варианта меня вполне устроила. Получалось, по прикидкам, тыщ до 10 слов в секунду для простых русских слов, около тыщи для навороченных. Для английского — еще быстрее. Но было 2 очевидных (еще до начала разработки) «но», связанных с тем, что все данные грузились в оперативную память (через pickle/cPickle). +1. первоначальная загрузка занимала 3-4 секунды +2. кушалось порядка 150 мегабайт оперативной памяти с psyco и порядка 100 без ( +удалось немного сократить, когда привел всякие там питоновские set и list к tulpe, где возможно) + +Не долго думая, провел небольшой рефакторинг и вынес все данные в отдельную сущность. А дальше мне на помощь пришла магия Python и duck typing. Вкратце — в алгоритмах использовались данные в виде ассоциативных и простых массивов. И все будет работать без переписывания алгоритмов, если вместо «настоящих» массивов подсунуть что-нибудь, что ведет себя как массив, а конкретнее, для нашей задачи, — поддерживает [] и in. Все) В стандартной поставке питона обнаружились такие «массивные» интерфейсы к нескольким нереляционным базам данных. Эти базы (bsddb, ndbm, gdbm), по сути, и есть ассоциативные массивы на диске. Как раз то, что нужно. Еще там обнаружилась пара высокоуровневых надстроек над этим хозяйством (anydbm и shelve). Остановился на том, что унаследовался от DbfilenameShelf и добавил поддержку ключей в юникоде и ключей-целых чисел. А, еще добавил кеширование результата, которое почему-то есть в Shelf, но убито в DbfilenameShelf. + +Данные по скорости на тестовых текстах (995 слов, русский словарь, macbook): +get_pickle_morph('ru', predict_by_prefix = False): 0.214 CPU seconds +get_pickle_morph('ru'): 0.262 CPU seconds +get_shelve_morph('ru', predict_by_prefix = False): 0.726 CPU seconds +get_shelve_morph('ru'): 0.874 CPU seconds + +Памяти вариант shelve, можно сказать, не кушал совсем. + +Варианты shelve похоже, работали, когда словари уже сидели в дисковом кеше. Без этого время может быть и 20 секунд с включенным предсказателем. Еще замечал, что медленнее всего работает предсказание по префиксу: до того, как прикрутил кеширование к своим наследникам от DbfilenameShelf, тормозило это предсказание раз в 5 больше, чем все остальное вместе взятое. А в этих тестах вроде не сильно уже. + +Кстати, пользуясь случаем, хочу спросить, вдруг кто-то знает, как в питоне можно узнать количество занятой текущим процессом памяти. По возможности кроссплатформенно как-нибудь. А то ставил в конец скрипта задержку и мерил по списку процессов. + +Пример использования + +import pymorphy +morph = pymorphy.get_shelve_morph('ru') +#слова должны быть в юникоде и ЗАГЛАВНЫМИ +info = morph.get_graminfo(unicode('Вася').upper()) + + +Так все-таки, зачем? + +В самом начале я уже объяснил, зачем стал писать морфологический анализатор. +Теперь время объяснить, почему я стал писать эту статью. Я ее написал в надежде на то, что такая библиотека интересна и полезна, и вместе мы эту библиотеку улучшим. Дело в том, что функционал, который уже реализован, вполне достаточен для моих нужд. Я буду ее поддерживать и исправлять, но не очень активно. Поэтому эта статья — не подробная инструкция по применению, а попытка описать, как все работает. + +В поставке + +pymorphy.py — сама библиотека +shelve_addons.py — наследники от shelf, может кому пригодится +encode_dicts.py — утилита, которая преобразовывает словари из формата AOT в форматы pymorphy. Без параметров, работает долго, ест метров 200 памяти, запускается 1 раз. Сконвертированные словари не распространяю из-за возможной бинарной несовместимости и большого объема. +test.py — юнит-тесты для pymorphy +example.py — небольшой пример и тексты с теми 995 словами +dicts/src — папка с исходными словарями, скачанными с aot.ru +dicts/converted — папка, куда encode_dicts.py будет складывать сконвертированные словари + +Напоследок + +ссылки: +www.assembla.com/wiki/show/pymorphy +hg.assembla.com/pymorphy +trac-hg.assembla.com/pymorphy + +Лицензия — MIT. + +проверял только под Python 2.5. + +Замечания, предложения, вопросы по делу — приветствуются. Если интересно — подключайтесь к разработке. \ No newline at end of file