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、前期準備
-
Lucene使用經驗
-
python使用經驗
-
相關工具包如下:
工具 | 版本 | 下載地址 |
---|---|---|
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包(
-
下載完解壓縮後的檔案包為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 =