1. 程式人生 > >NLP自然語言處理相關技術說明及樣例(附原始碼)

NLP自然語言處理相關技術說明及樣例(附原始碼)

https://segmentfault.com/a/1190000010320214

1、簡單概述

1.1 NLP概念

NLP(Natural Language Processing),自然語言處理,又稱NLU(Natural Language Understanding)自然語言理解,是語言資訊處理的分支,也是人工智慧的核心課題,簡單來說就是讓計算機理解自然語言。

1.2 NLP涉及的內容及技術

自然語言處理研究包含的內容十分廣泛,這裡只列舉出其中的其中的一部分(主要是在移動易系統中涉及到的),包括分詞處理(Word-Segment),詞性標註(Part-of-Speech tagging),句法分析(Parsing),資訊檢索(Infomation-Retrieval),文字校對(Text-Rroofing),詞向量模型(WordVector-Model),語言模型(Language-Model),問答系統(Question-Answer-System)。如下逐一介紹。

2、前期準備

  1. Lucene使用經驗

  2. python使用經驗

  3. 相關工具包如下:

工具 版本 下載地址
berkeleylm berkeleylm 1.1.5
ElasticSearch elasticsearch-2.4.5

3、具體實現

3.1 分詞(Word-Segment)

3.1.1 這裡主要介紹中文分詞的實現,實現中文分詞的方法有許多種,例如StandfordCore NLP(具體實現參見【NLP】使用 Stanford NLP 進行中文分詞 ),jieba分詞,這裡使用哈工大的語言技術平臺LTP(包括後面的詞性標註,句法分析)。具體步驟如下:

  • 首先下載LTP4J的jar包(

    download),

  • 下載完解壓縮後的檔案包為ltp4j-master,相應的jar包就在output資料夾下的jar資料夾中。

  • 下載編譯好的C++動態連結庫download,解壓後如下所示:  

  • 將資料夾中的所有內容複製到jdk的bin目錄下,如下所示:

  • 構建Java專案,將jar包匯入到專案中,右鍵專案buildpath,為新增的jar包新增本來地庫依賴,路勁即下載解壓後的dll動態庫檔案路徑,如下所示:

  • 接著便是中文分詞的測試了,實現程式碼如下:


    package ccw.ltpdemo;
    import java.util.ArrayList;
    import
java.util.List; import edu.hit.ir.ltp4j.Segmentor; public class ltpSegmentDemo { public static void main(String[] args) { Segmentor segmentor = new Segmentor(); if(segmentor.create("D:/NLP/ltp/ltp_data_v3.4.0/ltp_data_v3.4.0/cws.model")<0) { System.out.println("model load failed"); } else { String sent = "這是中文分詞測試"; List<String> words = new ArrayList<String>(); int size = segmentor.segment(sent, words); for(String word :words) { System.out.print(word+"\t"); } segmentor.release(); } } }

3.1.2 效果如下:

3.2 詞性標註(Part-of-Speech tagging)

3.2.1 這裡介紹如何通過ltp實現中文的詞性標註,具體實現程式碼如下:


    package ccw.ltpdemo;
    import java.util.ArrayList;
    import java.util.List;
    
    import edu.hit.ir.ltp4j.Postagger;
    public class ltpPostaggerDemo {
        public static void main(String[] args) {
            Postagger postagger = new Postagger();
            if(postagger.create("D:/NLP/ltp/ltp_data_v3.4.0/ltp_data_v3.4.0/pos.model")<0)
            {
                System.out.println("model load failed");
            }
            else
            {
                List<String> words = new ArrayList<String>();
                words.add("我");
                words.add("是");
                words.add("中國");
                words.add("人");
                List<String> values = new ArrayList<String>();
                
                int size = postagger.postag(words, values);
                for(int i = 0;i<words.size();i++)
                {
                    System.out.print(words.get(i)+" "+values.get(i)+"\t");
                }
                postagger.release();
            }
        }
    }

3.2.2 實現效果如下: 

3.3 句法分析(Parsing)

3.3.1 這裡介紹如何通過ltp實現對中文句子的句法分析,核心方法int size = Parser.parse(words,tags,heads,deprels),其中,words[]表示待分析的詞序列,tags[]表示待分析的詞的詞性序列,heads[]表示結果依存弧,heads[i]代表第i個節點的父節點編號(其中第0個表示根節點root),deprels[]表示依存弧的關係型別,size表示返回結果中詞的個數。實現程式碼如下:

    
    package ccw.ltpdemo;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import edu.hit.ir.ltp4j.Parser;
    
    public class ltpParserDemo {
        
        /**
         * @param args
         */
        public static void main(String[] args) {
            
            Parser parser = new Parser();
            if(parser.create("D:/NLP/ltp/ltp_data_v3.4.0/ltp_data_v3.4.0/parser.model")<0)
            {
                System.out.println("model load failed");
            }
            else
            {
                 List<String> words = new ArrayList<String>();
                    List<String> tags = new ArrayList<String>();
                    words.add("我");tags.add("r");
                    words.add("非常");tags.add("d");
                    words.add("喜歡");tags.add("v");
                    words.add("音樂");tags.add("n");
                    List<Integer> heads = new ArrayList<Integer>();
                    List<String> deprels = new ArrayList<String>();
                    int size = Parser.parse(words,tags,heads,deprels);
                    for(int i = 0;i<size;i++) {
                      System.out.print(heads.get(i)+":"+deprels.get(i));
                      if(i==size-1) {
                        System.out.println();
                      }
                      else{
                        System.out.print("        ");
                      }
                    }
    
                    parser.release();
            }
        }
    
    }

3.3.2 實現效果如下: 

3.4 資訊檢索(Information-Retrieval)

資訊檢索(Information Retrieval)是使用者進行資訊查詢和獲取的主要方式,是查詢資訊的方法和手段。狹義的資訊檢索僅指資訊查詢(Information Search)。即使用者根據需要,採用一定的方法,藉助檢索工具,從資訊集合中找出所需要資訊的查詢過程。實現參見移動易實現全文搜尋

3.5 文字校對(Text-Rroofing),語言模型(Language-Model)

3.5.1 N元模型(N-gram)

首先介紹N-gram模型,N-gram模型是自然語言處理中一個非常重要的概念,通常,在NLP中,基於一定的語料庫, 可以通過N-gram來預計或者評估一個句子是否合理。對於一個句子T,假設T由詞序列w1,w2,w3...wn組成,那麼T出現的概率

  • P(T)=P(w1,w2,w3...wn)=P(w1)P(w2|w1)P(w3|w2,w1)...p(wn|wn-1,...w2,w1)
    此概率在引數巨大的情況下顯然不容易計算,因此引入了馬爾可夫鏈(即每個詞出現的概率僅僅與它的前後幾個詞相關),這樣可以大幅度縮小計算的長度,即

  • P(wi|w1,⋯,wi−1)=P(wi|wi−n+1,⋯,wi−1)
    特別的,當n取值較小時:

當n=1時,即每一個詞出現的概率只由該詞的詞頻決定,稱為一元模型(unigram-model):

  • P(w1,w2,⋯,wm)=∏i=1mP(wi)
    設M表示語料庫中的總字數,c(wi)表示wi在語料庫中出現的次數,那麼

  • P(wi)=C(wi)/M
    當n=2時,即每一個詞出現的概率只由該詞的前一個詞以及後一個詞決定,稱為二元模型(bigram-model):

  • P(w1,w2,⋯,wm)=∏i=1mP(wi|wi−1) 
    設M表示語料庫中的總字數,c(wi-1WI)表示wi-1wi在語料庫中出現的次數,那麼

  • P(wi|wi−1)=C(wi−1wi)/C(wi−1)
    當n=3時,稱為三元模型(trigram-model):

  • P(w1,w2,⋯,wm)=∏i=1mP(wi|wi−2wi−1) 
    那麼

  • P(wi|wi−1wi-2)=C(wi-2wi−1wi)/C(wi−2wi-1)

3.5.2 中文拼寫糾錯

接著介紹如何通過Lucene提供的spellChecker(拼寫校正)模組實現中文字詞的糾錯,首先建立語料詞庫,如下所示:

然後在程式碼中建立索引並測試,具體實現程式碼如下:

    
      package ccw.spring.ccw.lucencedemo;
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.FileReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.util.Iterator;
      import org.apache.lucene.index.IndexReader;
      import org.apache.lucene.index.IndexWriterConfig;
      import org.apache.lucene.search.spell.LuceneDictionary;
      import org.apache.lucene.search.spell.PlainTextDictionary;
      import org.apache.lucene.search.spell.SpellChecker;
      import org.apache.lucene.search.suggest.InputIterator;
      import org.apache.lucene.store.Directory;
      import org.apache.lucene.store.FSDirectory;
      import org.apache.lucene.util.Version;
      public class Spellcheck {
        public static String directorypath;
        public static String origindirectorypath;
        public SpellChecker spellcheck;
        public LuceneDictionary dict;
        
    /**
     * 建立索引
     * a
     * @return
     * @throws IOException
     * boolean
     */
    public static void createIndex(String directorypath,String origindirectorypath) throws IOException
    {
        Directory directory = FSDirectory.open(new File(directorypath));
        
        SpellChecker spellchecker = new SpellChecker(directory);
        IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_9, null);
        PlainTextDictionary pdic = new PlainTextDictionary(new InputStreamReader(new FileInputStream(new File(origindirectorypath)),"utf-8"));
        spellchecker.indexDictionary(new PlainTextDictionary(new File(origindirectorypath)), config, false);
        directory.close();
        spellchecker.close();
    }
    public Spellcheck(String opath ,String path)
    {
        origindirectorypath = opath;
        directorypath = path;
        Directory directory;
        try {
               directory = FSDirectory.open(new File(directorypath));
               spellcheck = new SpellChecker(directory);
               IndexReader oriIndex = IndexReader.open(directory);
               dict = new LuceneDictionary(oriIndex,"name");
            }
         catch (IOException e) {
                     e.printStackTrace();
                 }
             
    }
    public void setAccuracy(float v)
    {
        spellcheck.setAccuracy(v);
    }
    
    
    public String[]search(String queryString, int suggestionsNumber)
    {
        String[]suggestions = null;
        try {
              if (exist(queryString))
              return null;
              suggestions = spellcheck.suggestSimilar(queryString,suggestionsNumber);
            }
             catch (IOException e) 
            {
              e.printStackTrace();
            }
            return suggestions;
    }
              
    private boolean exist(String queryString) throws IOException {
        InputIterator ite =  dict.getEntryIterator();
            while (ite.hasContexts())
              {
                  if (ite.next().equals(queryString))
                      return true;
              }
                     return false;
        }
    
    
    public static void main(String[] args) throws IOException {
        String opath = "D:\\Lucene\\NLPLucene\\words.txt";
        String ipath = "D:\\Lucene\\NLPLucene\\index";
        Spellcheck.createIndex(ipath, opath);
        Spellcheck spellcheck = new Spellcheck(opath,ipath);
        //spellcheck.createSpellIndex();
        
        spellcheck.setAccuracy((float) 0.5);
        String [] result = spellcheck.search("麻辣糖", 15);
        if(result.length==0||null==result)
        {
            System.out.println("未發現錯誤");
        }
        else
        {
            System.out.println("你是不是要找:");
            for(String hit:result)
            {
                System.out.println(hit);
            }
        }
    }
    
                 
    }

實現效果如下: 

3.5.3 中文語言模型訓練

這裡主要介紹中文語言模型的訓練,中文語言模型的訓練主要基於N-gram演算法,目前開源的語言模型訓練的工具主要有SRILM、KenLM、 berkeleylm 等幾種,KenLm較SRILM效能上要好一些,用C++編寫,支援單機大資料的訓練。berkeleylm是用java寫。本文主要介紹如何通過berkelylm實現中文語言模型的訓練。

  • 首先需要下載berkeleylm的jar包(download),完成後將jar包匯入到java專案中。

  • 然後準備訓練的語料庫,首先通過ltp將每一句文字分詞,然後將分完詞的語句寫入txt檔案,如下所示: 

  • 接著就是對語料庫的訓練,首先要讀取分完詞的文字,然後就是對每個詞計算在給定上下文中出現的概率,這裡的概率是對10取對數後計算得到的,最後將結果按照給定的格式儲存,可以按照.arpa或者二進位制.bin檔案儲存。檔案格式如下: 
      

實現程式碼如下:


    package ccw.berkeleylm;
    
    import java.io.File;
    import java.util.ArrayList;
    import java.util.List;
    
    import edu.berkeley.nlp.lm.ConfigOptions;
    import edu.berkeley.nlp.lm.StringWordIndexer;
    import edu.berkeley.nlp.lm.io.ArpaLmReader;
    import edu.berkeley.nlp.lm.io.LmReaders;
    import edu.berkeley.nlp.lm.util.Logger;
    
    public class demo {
        
        
        private static void usage() {
            System.err.println("Usage: <lmOrder> <ARPA lm output file> <textfiles>*");
            System.exit(1);
        }
        
        public void makelml(String [] argv)
        {
            if (argv.length < 2) {
                usage();
            }
            final int lmOrder = Integer.parseInt(argv[0]);
            final String outputFile = argv[1];
            final List<String> inputFiles = new ArrayList<String>();
            for (int i = 2; i < argv.length; ++i) {
                inputFiles.add(argv[i]);
            }
            if (inputFiles.isEmpty()) inputFiles.add("-");
            Logger.setGlobalLogger(new Logger.SystemLogger(System.out, System.err));
            Logger.startTrack("Reading text files " + inputFiles + " and writing to file " + outputFile);
            final StringWordIndexer wordIndexer = new StringWordIndexer();
            wordIndexer.setStartSymbol(ArpaLmReader.START_SYMBOL);
            wordIndexer.setEndSymbol(ArpaLmReader.END_SYMBOL);
            wordIndexer.setUnkSymbol(ArpaLmReader.UNK_SYMBOL);
            LmReaders.createKneserNeyLmFromTextFiles(inputFiles, wordIndexer, lmOrder, new File(outputFile), new ConfigOptions());
            Logger.endTrack();
        }
        
        public static void main(String[] args) {
            
            demo d = new demo();
            String inputfile = "D:\\NLP\\languagematerial\\quest.txt";
            String outputfile = "D:\\NLP\\languagematerial\\q.arpa";
            String s[]={"8",outputfile,inputfile};
            d.makelml(s);
            
        }
    
    }
  • 最後就是讀取模型,然後判斷句子的相似性,實現程式碼如下:


    package ccw.berkeleylm;
    
    import java.io.File;
    import java.util.ArrayList;
    import java.util.List;
    
    import edu.berkeley.nlp.lm.ArrayEncodedProbBackoffLm;
    import edu.berkeley.nlp.lm.ConfigOptions;
    import edu.berkeley.nlp.lm.StringWordIndexer;
    import edu.berkeley.nlp.lm.io.LmReaders;
    
    public class readdemo {
        
        public static ArrayEncodedProbBackoffLm<String> getLm(boolean compress,String file) {
            final File lmFile = new File(file);
            final ConfigOptions configOptions = new ConfigOptions();
            configOptions.unknownWordLogProb = 0.0f;
            final ArrayEncodedProbBackoffLm<String> lm = LmReaders.readArrayEncodedLmFromArpa(lmFile.getPath(), compress, new StringWordIndexer(), configOptions,
                Integer.MAX_VALUE);
            return lm;
        }
        
        
        public static void main(String[] args) {
            readdemo read = new readdemo();
            LmReaders readers = new LmReaders();
            ArrayEncodedProbBackoffLm<String> model = (ArrayEncodedProbBackoffLm) readdemo.getLm(false, "D:\\NLP\\languagematerial\\q.arpa");
            String sentence = "是";
            String [] words = sentence.split(" ");
            List<String> list = new ArrayList<String>();
            for(String word : words)
            {
                System.out.println(word);
                list.add(word);
            }
            float score = model.getLogProb(list);
            System.out.println(score);
        }
    
    }

實現效果如下:

 

3.5.4 同義詞詞林

這裡使用哈工大提供的同義詞詞林,詞林提供三層編碼,第一級大類用大寫英文字母表示,第二級中類用小寫字母表示,第三級小類用二位十進位制整數表示,第四級詞群用大寫英文字母表示,第五級原子詞群用二位十進位制整數表示。編碼表如下所示:

   

第八位的標記有三種,分別是“=“、”#“、”@“,=代表相等、同義,#代表不等、同類,@代表自我封閉、獨立,它在詞典中既沒有同義詞,也沒有相關詞。通過同義詞詞林可以比較兩詞的相似程度,程式碼實現如下:


    package cilin;
    import java.io.BufferedReader;
    import java.io.FileInputStream;
    import java.io.InputStreamReader;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Vector;
    public class CiLin {
    public static HashMap<String, List<String>> keyWord_Identifier_HashMap;//<關鍵詞,編號List集合>雜湊
    
    public int zero_KeyWord_Depth = 12;
    public static HashMap<String, Integer> first_KeyWord_Depth_HashMap;//<第一層編號,深度>雜湊
    public static HashMap<String, Integer> second_KeyWord_Depth_HashMap;//<前二層編號,深度>雜湊
    public static HashMap<String, Integer> third_KeyWord_Depth_HashMap;//<前三層編號,深度>雜湊
    public static HashMap<String, Integer> fourth_KeyWord_Depth_HashMap;//<前四層編號,深度>雜湊
    //public HashMap<String, HashSet<String>> ciLin_Sort_keyWord_HashMap = new HashMap<String, HashSet<String>>();//<(同義詞)編號,關鍵詞Set集合>雜湊
    
    static{
        keyWord_Identifier_HashMap = new HashMap<String, List<String>>();
        first_KeyWord_Depth_HashMap = new HashMap<String, Integer>();
        second_KeyWord_Depth_HashMap = new HashMap<String, Integer>();
        third_KeyWord_Depth_HashMap = new HashMap<String, Integer>();
        fourth_KeyWord_Depth_HashMap = new HashMap<String, Integer>();
        initCiLin();
    }
    
    //3.初始化詞林相關
    public static void initCiLin(){
        int i;
        String str = null;
        String[] strs = null;
        List<String> list = null;
        BufferedReader inFile = null;
        try {
            //初始化<關鍵詞, 編號set>雜湊
            inFile = new BufferedReader(new InputStreamReader(new FileInputStream("cilin/keyWord_Identifier_HashMap.txt"), "utf-8"));// 讀取文字
            while((str = inFile.readLine()) != null){
                strs = str.split(" ");
                list = new Vector<String>();
                for (i = 1; i < strs.length; i++) 
                    list.add(strs[i]);
                keyWord_Identifier_HashMap.put(strs[0], list);
            }
            
            //初始化<第一層編號,高度>雜湊
            inFile.close();
            inFile = new BufferedReader(new InputStreamReader(new FileInputStream("cilin/first_KeyWord_Depth_HashMap.txt"), "utf-8"));// 讀取文字
            while ((str = inFile.readLine()) != null){
                strs =