N-gram是常用的概率語(yǔ)言模型试疙,可以通過(guò)已有語(yǔ)料推斷語(yǔ)句結(jié)構(gòu)的合理性祝旷,在自然語(yǔ)言處理中有著廣泛的應(yīng)用嘶窄,N-gram的概念就不多說(shuō)了,網(wǎng)上有大把的教程吻谋,想了解的可以自己搜现横。
Stream是java8的新特性戒祠,java8已經(jīng)發(fā)布3年有余了,不知道大家在實(shí)際中應(yīng)用的有多少姜盈,工作原因這兩年java代碼寫的比較少,就拿N-gram算法來(lái)練練手示血,個(gè)人感覺stream還是很適合做文字處理這種事情的矾芙,流式編程寫起來(lái)還是很方便的近上。
下面來(lái)看看具體實(shí)現(xiàn):
package nlp.ngram;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Ngram {
private static Map<String, AtomicInteger> trainingData;
static {
String path = ".zhuxian.txt";
trainingData = initTrainingData(path);//初始化訓(xùn)練數(shù)據(jù),加載到內(nèi)存
}
/**
* 計(jì)算輸入句子的合理性概率
* @param sentence 要評(píng)估的語(yǔ)句
* @param n N-gram的N
* @return 合理性概率
*/
private static float getProbability(String sentence, int n) {
final String sen = "@" + sentence + "#";
return Stream
.iterate(0, i -> ++i)
.limit(sen.length() - n + 1)
.map(start -> sen.substring(start, start + n))
.map(s -> {
float nu = (float) (null == trainingData.get(s) ? 1 : trainingData.get(s).get() + 1);
float de = (float) (null == trainingData.get(s.substring(0, s.length() - 1)) ? 1 : trainingData.get
(s.substring(0, s.length() - 1)).get() + 1);
System.out.println(s + "/" + s.substring(0, s.length() - 1) + " " + nu / de);
return nu / de;
})
.reduce(1f, (f1, f2) -> f1 * f2);
}
/**
* 把訓(xùn)練數(shù)據(jù)處理過(guò)后加載到內(nèi)存葱绒,統(tǒng)計(jì)每個(gè)分詞的出現(xiàn)頻次
* @param path 訓(xùn)練集路徑
* @return 統(tǒng)計(jì)數(shù)據(jù)map
*/
private static Map<String, AtomicInteger> initTrainingData(String path) {
return readFileOrDir(path)
.map(s -> s.replaceAll("[”“\\w\\s《》.::*‘’地淀、\"<>\\[\\]^`~]", ""))//去掉文字里的無(wú)意義字符,這里只處理中文
.flatMap(s -> Stream.of(s.split("[岖是,,帮毁。实苞;;!!烈疚??]")))//分割句子
.filter(s -> !"".equals(s))//去掉空行
// .peek(System.out::println)
.map(s -> "@" + s + "#")//加上句首和句尾標(biāo)記
.flatMap(s -> Stream
.iterate(1, i -> ++i)//支持的N-gram的N為1黔牵、2、3爷肝、4
.limit(s.length() > 4 ? 4 : s.length())//N-gram的N最大為4猾浦,太大了內(nèi)存容易爆,實(shí)際應(yīng)用中4基本就夠了
.parallel()
.flatMap(n -> Stream
.iterate(0, i -> ++i)
.limit(s.length() - n + 1)
.parallel()
.map(start -> s.substring(start, start + n))//分割句子為n個(gè)字的集合
)
)
.collect(Collectors.toConcurrentMap(o -> o,
o -> new AtomicInteger(1), (e1, e2) -> {
e1.incrementAndGet();
return e1;
}));
}
/**
* 文件讀取工具灯抛,以行為單位輸出
* @param path 文件路徑
* @return Stream lines 流
*/
private static Stream<String> readFileOrDir(String path) {
File file = new File(path);
if (file.isDirectory()) {
String[] paths = file.list((dir, name) -> !name.startsWith("."));
assert paths != null;
return Arrays.stream(paths)
.flatMap(p -> readFileOrDir(path + File.separator + p));
} else {
try {
return new BufferedReader(new java.io.FileReader(path)).lines();
} catch (Exception e) {
System.err.println("read file " + path + " error!" + e.getMessage());
return Stream.empty();
}
}
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.lines().forEach(s -> System.out.println(getProbability(s, 3)));
}
}
這里用java流式編程可以大大的減少代碼量金赦,一個(gè)方法處理語(yǔ)料,一個(gè)方法計(jì)算概率就完事了对嚼,對(duì)于cpu密集型的任務(wù)還可以用多線程 (parallel) 來(lái)加速處理速度夹抗,不過(guò)并發(fā)的坑就得自己填了磨确。
這里的實(shí)現(xiàn)相當(dāng)基礎(chǔ)声邦,沒有分詞邓了,準(zhǔn)確性會(huì)低很多骗炉,可優(yōu)化的空間還很大句葵,還有數(shù)據(jù)的平滑 (smoothing) 處理這里就不展開討論了乍丈,這里的實(shí)現(xiàn)只是簡(jiǎn)單的把沒出現(xiàn)的詞出現(xiàn)的次數(shù)設(shè)為1轻专,實(shí)際使用中要結(jié)合實(shí)際設(shè)計(jì)數(shù)據(jù)平滑算法请垛,有時(shí)間再填填優(yōu)化的部分宗收。