working on morh

git-svn-id: https://russianmorphology.googlecode.com/svn/trunk@38 d817d54c-26ab-11de-abc9-2f7d1455ff7a
This commit is contained in:
alexander.a.kuznetsov 2009-08-11 16:16:56 +00:00
parent e4dd3a7a76
commit 70842ecfb7
17 changed files with 468 additions and 200 deletions

View File

@ -16,20 +16,15 @@
package org.apache.lucene.russian.morphology; package org.apache.lucene.russian.morphology;
import org.apache.lucene.russian.morphology.dictonary.DictonaryReader; import org.apache.lucene.russian.morphology.dictonary.*;
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.heuristic.HeuristicBySuffixLegth; import org.apache.lucene.russian.morphology.heuristic.HeuristicBySuffixLegth;
import org.apache.lucene.russian.morphology.heuristic.StatiticsCollectors; import org.apache.lucene.russian.morphology.heuristic.StatiticsCollectors;
import org.apache.lucene.russian.morphology.heuristic.SuffixCounter; 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.io.IOException;
import java.util.Arrays; import java.util.*;
import java.util.Collection; import java.util.concurrent.atomic.AtomicLong;
import java.util.Set;
import java.util.TreeMap;
public class HeuristicBuilder { public class HeuristicBuilder {
@ -57,47 +52,28 @@ public class HeuristicBuilder {
heuristic.addHeuristic(((SuffixCounter) objects[i]).getSuffixHeuristic()); heuristic.addHeuristic(((SuffixCounter) objects[i]).getSuffixHeuristic());
} }
TreeMap<Integer, Integer> map = new TreeMap<Integer, Integer>(); final Map<Long,Set<SimpleSuffixHeuristic>> map = heuristic.getUnkowns();
int ct = 0; //final RussianSuffixDecoderEncoder decoderEncoder = new RussianSuffixDecoderEncoder(6);
for (Set<SuffixHeuristic> s : heuristic.getHeuristics().values()) { final AtomicLong c = new AtomicLong(0L);
Integer d = map.get(s.size()); final AtomicLong all = new AtomicLong(0L);
map.put(s.size(), 1 + (d == null ? 0 : d)); dictonaryReader.proccess(
if (s.size() == 1) { new WordProccessor(){
ct++; public void proccess(WordCard wordCard) throws IOException {
continue; for (FlexiaModel fm : wordCard.getWordsFroms()) {
} String form = fm.create(wordCard.getBase());
SuffixHeuristic heuristic1 = s.iterator().next(); int startSymbol = form.length() > RussianSuffixDecoderEncoder.suffixLength ? form.length() - RussianSuffixDecoderEncoder.suffixLength : 0;
Integer sufixSize = heuristic1.getActualSuffixLength(); String formSuffix = form.substring(startSymbol);
String normalSuffix = heuristic1.getNormalFromSuffix(); Long aLong = RussianSuffixDecoderEncoder.encode(formSuffix);
if (heuristic1.getFormSuffix().length() < 6) { all.incrementAndGet();
ct++; if(map.containsKey(aLong)) c.incrementAndGet();
continue; }
} }
Boolean flag = true; }
if (sufixSize > 3) continue; );
for (SuffixHeuristic sh : s) {
flag = flag && (sufixSize.equals(sh.getActualSuffixLength()))
&& (normalSuffix.equals(sh.getNormalFromSuffix())); System.out.println("Ankown words " + all.longValue());
} System.out.println("Ankown words " + c.longValue());
if (flag) {
System.out.println(s);
ct++;
}
//HashSet<String> integers = new HashSet<String>();
// 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");
} }
} }

View File

@ -24,13 +24,17 @@ package org.apache.lucene.russian.morphology;
*/ */
public class RussianSuffixDecoderEncoder { public class RussianSuffixDecoderEncoder {
public static final int RUSSIAN_SMALL_LETTER_OFFSET = 1071; 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 EE_CHAR = 34;
public static final int E_CHAR = 6; public static final int E_CHAR = 6;
public static final int DASH_CHAR = 45; public static final int DASH_CHAR = 45;
public static final int DASH_CODE = 33; public static final int DASH_CODE = 33;
public RussianSuffixDecoderEncoder(int suffixLength) {
RussianSuffixDecoderEncoder.suffixLength = suffixLength;
}
static public Long encode(String string) { static public Long encode(String string) {
if (string.length() > 12) throw new SuffixToLongException("Suffix length should not be greater then " + 12); if (string.length() > 12) throw new SuffixToLongException("Suffix length should not be greater then " + 12);
long result = 0L; long result = 0L;

View File

@ -8,6 +8,6 @@ import java.io.IOException;
public class Test { public class Test {
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
GrammaReader grammaReader = new GrammaReader("dictonary/Dicts/Morph/rgramtab.tab"); GrammaReader grammaReader = new GrammaReader("dictonary/Dicts/Morph/rgramtab.tab");
System.out.println(grammaReader.getInversIndex().size()); //System.out.println(grammaReader.getInversIndex().size());
} }
} }

View File

@ -58,7 +58,7 @@ public class SuffixHeuristic {
} }
public String getCanonicalForm(String form) { 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); String suffixS = form.substring(startSymbol);
if (!chechSuffix(suffixS)) return form; if (!chechSuffix(suffixS)) return form;

View File

@ -6,12 +6,14 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.List;
import java.util.ArrayList;
//todo spleet this class on two. //todo spleet this class on two.
public class GrammaReader { public class GrammaReader {
private String fileName; private String fileName;
private String fileEncoding = "windows-1251"; private String fileEncoding = "windows-1251";
private Map<Integer, String> grammaInfo = new HashMap<Integer, String>(); private List<String> grammaInfo = new ArrayList<String>();
private Map<String, Integer> inversIndex = new HashMap<String, Integer>(); private Map<String, Integer> inversIndex = new HashMap<String, Integer>();
public GrammaReader(String fileName) throws IOException { public GrammaReader(String fileName) throws IOException {
@ -34,21 +36,17 @@ public class GrammaReader {
String[] strings = line.split(" ", 2); String[] strings = line.split(" ", 2);
Integer i = grammaInfo.size(); Integer i = grammaInfo.size();
inversIndex.put(strings[0], i); inversIndex.put(strings[0], i);
grammaInfo.put(i, strings[1]); grammaInfo.add(i, strings[1]);
} }
line = bufferedReader.readLine(); line = bufferedReader.readLine();
} }
} }
public Map<Integer, String> getGrammaInfo() { public List<String> getGrammaInfo() {
return grammaInfo; return grammaInfo;
} }
public void setGrammaInfo(Map<Integer, String> grammaInfo) { public Map<String, Integer> getGrammInversIndex() {
this.grammaInfo = grammaInfo;
}
public Map<String, Integer> getInversIndex() {
return inversIndex; return inversIndex;
} }

View File

@ -28,16 +28,16 @@ import java.util.TreeMap;
public class Heuristic { public class Heuristic {
private TreeMap<Long, Long> encodedSuffixesPairs = new TreeMap<Long, Long>(); private TreeMap<Long, Long> encodedSuffixesPairs = new TreeMap<Long, Long>();
public void addHeuristic(SuffixHeuristic suffixHeuristic) { public void addHeuristic(SimpleSuffixHeuristic simpleSuffixHeuristic) {
// Long suffix = RussianSuffixDecoderEncoder.encode(suffixHeuristic.getFormSuffix()); // Long suffix = RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getFormSuffix());
// Long longs = encodedSuffixesPairs.get(suffix); // Long longs = encodedSuffixesPairs.get(suffix);
// if (longs == null) { // if (longs == null) {
// encodedSuffixesPairs.put(suffix, RussianSuffixDecoderEncoder.encode(suffixHeuristic.getNormalSuffix())); // encodedSuffixesPairs.put(suffix, RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getNormalSuffix()));
// } // }
} }
public String getNormalForm(String form) { 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 suffix = RussianSuffixDecoderEncoder.encode(form.substring(startSymbol));
Long normalSuffix = encodedSuffixesPairs.get(suffix); Long normalSuffix = encodedSuffixesPairs.get(suffix);

View File

@ -9,19 +9,77 @@ import java.util.Set;
public class HeuristicBySuffixLegth { public class HeuristicBySuffixLegth {
private Map<Long, Set<SuffixHeuristic>> heuristics = new HashMap<Long, Set<SuffixHeuristic>>(); private Map<Long, Set<SimpleSuffixHeuristic>> heuristics = new HashMap<Long, Set<SimpleSuffixHeuristic>>();
public void addHeuristic(SuffixHeuristic suffixHeuristic) { public void addHeuristic(SimpleSuffixHeuristic simpleSuffixHeuristic) {
Long suffix = RussianSuffixDecoderEncoder.encode(suffixHeuristic.getFormSuffix()); Long suffix = RussianSuffixDecoderEncoder.encode(simpleSuffixHeuristic.getFormSuffix());
Set<SuffixHeuristic> suffixHeuristics = heuristics.get(suffix); Set<SimpleSuffixHeuristic> simpleSuffixHeuristics = heuristics.get(suffix);
if (suffixHeuristics == null) { if (simpleSuffixHeuristics == null) {
suffixHeuristics = new HashSet<SuffixHeuristic>(); simpleSuffixHeuristics = new HashSet<SimpleSuffixHeuristic>();
heuristics.put(suffix, suffixHeuristics); heuristics.put(suffix, simpleSuffixHeuristics);
} }
suffixHeuristics.add(suffixHeuristic); simpleSuffixHeuristics.add(simpleSuffixHeuristic);
} }
public Map<Long, Set<SuffixHeuristic>> getHeuristics() { public Map<Long, Set<SimpleSuffixHeuristic>> getHeuristics() {
return heuristics; return heuristics;
} }
public Map<Long,SimpleSuffixHeuristic> getSingleSuffixes(){
HashMap<Long, SimpleSuffixHeuristic> result = new HashMap<Long, SimpleSuffixHeuristic>();
for(Long st:heuristics.keySet()){
if(heuristics.get(st).size() == 1){
result.put(st,heuristics.get(st).iterator().next());
}
}
return result;
}
public Map<Long,Set<SimpleSuffixHeuristic>> getWordWithMorphology(){
HashMap<Long, Set<SimpleSuffixHeuristic>> result = new HashMap<Long, Set<SimpleSuffixHeuristic>>();
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<Long,Set<SimpleSuffixHeuristic>> getOnonyms(){
HashMap<Long, Set<SimpleSuffixHeuristic>> result = new HashMap<Long, Set<SimpleSuffixHeuristic>>();
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<Long,Set<SimpleSuffixHeuristic>> getUnkowns(){
HashMap<Long, Set<SimpleSuffixHeuristic>> result = new HashMap<Long, Set<SimpleSuffixHeuristic>>();
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<SimpleSuffixHeuristic> 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;
}
} }

View File

@ -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;
}
}

View File

@ -27,7 +27,7 @@ import java.util.Map;
public class StatiticsCollectors implements WordProccessor { public class StatiticsCollectors implements WordProccessor {
Map<SuffixHeuristic, SuffixCounter> statititics = new HashMap<SuffixHeuristic, SuffixCounter>(); Map<SimpleSuffixHeuristic, SuffixCounter> statititics = new HashMap<SimpleSuffixHeuristic, SuffixCounter>();
private Map<String, Double> wordsFreq; private Map<String, Double> wordsFreq;
private GrammaReader grammaInfo; private GrammaReader grammaInfo;
@ -40,12 +40,12 @@ public class StatiticsCollectors implements WordProccessor {
public void proccess(WordCard wordCard) { public void proccess(WordCard wordCard) {
for (FlexiaModel fm : wordCard.getWordsFroms()) { for (FlexiaModel fm : wordCard.getWordsFroms()) {
SuffixHeuristic suffixHeuristic = createEvristic(wordCard.getCanonicalFrom(), wordCard.getCanonicalSuffix(), fm); SimpleSuffixHeuristic simpleSuffixHeuristic = createEvristic(wordCard.getBase(), wordCard.getCanonicalSuffix(), fm);
if (suffixHeuristic == null) continue; if (simpleSuffixHeuristic == null) continue;
SuffixCounter suffixCounter = statititics.get(suffixHeuristic); SuffixCounter suffixCounter = statititics.get(simpleSuffixHeuristic);
if (suffixCounter == null) { if (suffixCounter == null) {
suffixCounter = new SuffixCounter(suffixHeuristic); suffixCounter = new SuffixCounter(simpleSuffixHeuristic);
statititics.put(suffixHeuristic, suffixCounter); statititics.put(simpleSuffixHeuristic, suffixCounter);
} }
Double freq = wordsFreq.get(wordCard.getCanonicalFrom()); Double freq = wordsFreq.get(wordCard.getCanonicalFrom());
if (freq != null) { if (freq != null) {
@ -57,27 +57,17 @@ public class StatiticsCollectors implements WordProccessor {
} }
} }
public Map<SuffixHeuristic, SuffixCounter> getStatititics() { public Map<SimpleSuffixHeuristic, SuffixCounter> getStatititics() {
return statititics; 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); 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 formSuffix = form.substring(startSymbol);
String actualSuffix = fm.getSuffix(); String actualSuffix = fm.getSuffix();
Integer actualSuffixLengh = actualSuffix.length(); Integer actualSuffixLengh = actualSuffix.length();
// if (word.length() < startSymbol) { return new SimpleSuffixHeuristic(formSuffix, actualSuffixLengh, canonicalSuffix, fm.getCode());
// 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());
} }

View File

@ -21,11 +21,11 @@ package org.apache.lucene.russian.morphology.heuristic;
* in dictionary. * in dictionary.
*/ */
public class SuffixCounter implements Comparable { public class SuffixCounter implements Comparable {
private SuffixHeuristic suffixHeuristic; private SimpleSuffixHeuristic simpleSuffixHeuristic;
private Double amnout = 0.0; private Double amnout = 0.0;
public SuffixCounter(SuffixHeuristic suffixHeuristic) { public SuffixCounter(SimpleSuffixHeuristic simpleSuffixHeuristic) {
this.suffixHeuristic = suffixHeuristic; this.simpleSuffixHeuristic = simpleSuffixHeuristic;
} }
public void incrementAmount() { public void incrementAmount() {
@ -36,12 +36,12 @@ public class SuffixCounter implements Comparable {
amnout += wordFreq; amnout += wordFreq;
} }
public SuffixHeuristic getSuffixHeuristic() { public SimpleSuffixHeuristic getSuffixHeuristic() {
return suffixHeuristic; return simpleSuffixHeuristic;
} }
public void setSuffixEvristic(SuffixHeuristic suffixHeuristic) { public void setSuffixEvristic(SimpleSuffixHeuristic simpleSuffixHeuristic) {
this.suffixHeuristic = suffixHeuristic; this.simpleSuffixHeuristic = simpleSuffixHeuristic;
} }
public Double getAmnout() { public Double getAmnout() {
@ -59,6 +59,6 @@ public class SuffixCounter implements Comparable {
@Override @Override
public String toString() { public String toString() {
return "" + amnout + " " + suffixHeuristic.toString(); return "" + amnout + " " + simpleSuffixHeuristic.toString();
} }
} }

View File

@ -1,85 +1,10 @@
/** package org.apache.lucene.russian.morphology.heuristic;
* Copyright 2009 Alexander Kuznetsov
*
* Licensed under the Apache License, Version 2.0 (the "License"); public class SuffixHeuristic {
* you may not use this file except in compliance with the License. private SuffixTypes suffixType;
* You may obtain a copy of the License at private Byte suffixLengh;
* private Short indexOfWordTransorm;
* http://www.apache.org/licenses/LICENSE-2.0 private Short indexOfMothInfo;
* }
* 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
package org.apache.lucene.russian.morphology.heuristic;
public enum SuffixTypes {
SINGLE,
DIFFIRENT_MORPH,
ONONIMS
}

View File

@ -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];
}
}

View File

@ -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];
}
}

View File

@ -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." );
}
}

View File

@ -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.
Замечания, предложения, вопросы по делу — приветствуются. Если интересно — подключайтесь к разработке.