Android语音识别之模糊匹配
一、语音识别库的问题最近在做语音识别,使用了科大讯飞的语音识别库。首先得说,这个识别率挺高的。可是终究还是有时会出错,特别是一些易混音,例如sh s,l n等。比如我说“年级”给识别成“联机”了,拼音没学好啊。可是,又有多少人敢说自己发音是完全标准的呢?有没有办法提升正确率呢?毕竟,这种问题,很多人都会遇到呀。又继续研究了一下,原来,科大讯飞语音识别库已经有针对某些常用场景的,例如订
一、语音识别库的问题
最近在做语音识别,使用了科大讯飞的语音识别库。
首先得说,这个识别率挺高的。
可是终究还是有时会出错,特别是一些易混音,例如sh s,l n等。
比如我说“年级”给识别成“联机”了,拼音没学好啊。可是,又有多少人敢说自己发音是完全标准的呢?
有没有办法提升正确率呢?毕竟,这种问题,很多人都会遇到呀。
又继续研究了一下,原来,科大讯飞语音识别库已经有针对某些常用场景的,例如订餐、旅行、天气等,有特定识别库。但是对于小众的特定识别场景,需要进行特定开发,是需要费用的。
我只是一个小应用,哪敢麻烦他们进行针对性开发呀。
自己又没有做语音识别的本领,怎么办?
当时没有思路,这个事情就搁置了,但是这个事情还是放在心里。后来,一个偶然的机会,看到有一篇文章介绍汉字转拼音的,然后就有了思路:
在科大讯飞语音识别的基础上,按照拼音相似性进行模糊匹配。
二、模糊匹配的思路
思路是这样的:
1,将目标字符集转换为拼音;
2,获取科大讯飞语音识别的结果;
3,将识别结果转换为拼音;
4,在目标拼音集中进行拼音的匹配查找;
5,对于查找不到的,进行易混拼音的替换,再次与目标集合匹配查找;
6,对于还没有查找到的,去掉音调,再次与目标集合匹配查找;
7,将模糊匹配后的结果展现出来;
三、目标字符集
首先将可能的字符,分为3大类:
1,汉字
2,数字
3,英文字母
然后,获取这3类字符的拼音:
汉字转拼音,有现成的库:HanziToPinyin。
阿拉伯数字,先转换为汉字的数字,也能获取到拼音;
英文字母:确实没有现成的,反正才26个,自己拼呗。
下面是我准备好的A~Z的字母对应的拼音数组,不一定很标准,用于拼音相似度检查已经足够了:
String[] englishPinYin26={
"EI1", "BI4", "SEI4", "DI4", "YI4", "EFU1", "JI4",
"EIQI1", "AI4", "JEI4", "KEI4", "EOU1", "EMEN1", "EN1",
"OU1", "PI1", "KIU1", "A4", "ESI1", "TI4",
"YOU4", "WEI4", "DABULIU3", "EKESI1", "WAI4", "ZEI4"
};
汉字转拼音,使用的是HanziToPinyin,为了尽可能的匹配准确,所以使用了音调。将一个汉字字符串转换为拼音的字符串List,方法如下:
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
str = specialHanziString;//汉字
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
String[] vals = PinyinHelper.toHanyuPinyinStringArray(c, format);
specialHanziPinYin.add(vals[0]);
}
由于数字的转换方法是类似的,这里就不展示代码了,感兴趣的可以从demo中去看。
四、易混拼音
对于模糊匹配,我首先根据自己发音不准的情况,总结了一下易错的拼音。然后,到网上搜索了一下,居然真就是这几种情况!原来,我的拼音的错误情况,已经包含了所有易错拼音,不知是该高兴还是该惭愧,汗!
这里是3种易混音:
平舌音、翘舌音: zh,ch,sh z c s
前鼻音和后鼻音: ang,eng,ing an,en,in
鼻音N与边音L: l n
五、易混拼音替换
知道了哪些是易混的,剩下的就是设计一个方案来进行易混拼音的替换了。
我是这样做的:
检查准备替换的字符,是否符合易混的规则,也就是说,先检查下第一个字母,是不是z、c、s、l、n中的一个,若有,则进行相应相似判断后进行替换。
然后检查拼音字符串中是否存在ang、eng、ing,若有,则替换为an、en、in;
然后检查拼音字符串中是否存在an、en、in,若有,则替换为ang、eng、ing;
这里需要注意下,就是首声母与韵母可能都需要替换。举个例子:san:
替换声母,得到shan;
替换韵母,得到sang;
同时替换声母与韵母,得到shang;
六、以一个实例来展示
我的目标是,使用语音输入,输入的内容是:
某年级某班,多少分。
在录入后,进行模糊匹配,然后添加到表格中。
这样,我的目标集合就是:
汉字,阿拉伯数字
由于阿拉伯数字最终也是转换为汉字,所以我的目标字符集合是这样的:
String numberString="零一二三四五六七八九十百点";
String specialHanziString="年级班分";
为什么数字中有个“点”呢,是考虑到分数也有带小数的,例如97.5。
七、核心的拼音模糊匹配代码:
光是核心代码也是挺长的,按照对外的接口,分为2部分来说明:
1,类的成员及构造函数;
2,对外的转换接口changeOurWordsWithPinyin(),以及其中的转换细节描述;
先来看看第一部分,类的成员及构造函数:
/*
* 日期 : 2016年12月7日<br>
* 作者 : lintax<br>
* 功能 : 分析拼音相似性,进行字符替换<br>
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
/**
* An object to convert Chinese character to its corresponding pinyin string.
* Change word to our target word.
*/
public class PinyinSimilarity {
String[] englishPinYin26={
"EI1", "BI4", "SEI4", "DI4", "YI4", "EFU1", "JI4",
"EIQI1", "AI4", "JEI4", "KEI4", "EOU1", "EMEN1", "EN1",
"OU1", "PI1", "KIU1", "A4", "ESI1", "TI4",
"YOU4", "WEI4", "DABULIU3", "EKESI1", "WAI4", "ZEI4"
};
String englishString26="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String numberStringArabic="0123456789";
String numberString="零一二三四五六七八九十百点";
String specialHanziString="年级班分";
String myCharAll = numberString + specialHanziString;
List<String> numberPinYin=new ArrayList<String>(20);//数字的拼音(10)
List<String> specialHanziPinYin=new ArrayList<String>(10);//特定汉字集的拼音(除了中文的数字之外的)
List<String> myCharAllPinYin=new ArrayList<String>(40);//所有拼音的集合
boolean fuzzyMatching=true;//是否开启模糊匹配功能
public PinyinSimilarity(boolean fuzzyMatching){
this.fuzzyMatching = fuzzyMatching;
init();
}
这里要说明一下,在这个demo中,目标字符集是“年级班分0123456789.”。由于数字的发音,与数值的大小有关系,而我们这里的数组表示分数,最多只是100分,所以数字对应的汉字集合是”零一二三四五六七八九十百点”。
在构造函数中,有一个关键函数init(),主要作用是获取到目标字符集的拼音列表,下面是具体实现:
//拼音中有音标
//初始化目标汉字集的拼音列表
public void init()
{
try{
String str ;
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
str = numberString;//数字
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
String[] vals = PinyinHelper.toHanyuPinyinStringArray(c, format);
numberPinYin.add(vals[0]);
}
str = specialHanziString;//汉字
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
String[] vals = PinyinHelper.toHanyuPinyinStringArray(c, format);
specialHanziPinYin.add(vals[0]);
}
myCharAllPinYin.addAll(numberPinYin);
myCharAllPinYin.addAll(specialHanziPinYin);
} catch (Exception e){
e.printStackTrace();
}
}
下面这个函数,是对外的主要接口,实现了核心功能—-相似拼音的替换:
public String changeOurWordsWithPinyin(String input){
String output=input;
try{
//处理符号:不关注符合,遇到,就去掉(要保留小数点)
output = changeWordProcessSignal(output);
//处理英文字母:转大写
output = changeWordProcessEnglish(output);
//所有汉字进行相似替换
LogUtil.logWithMethod(new Exception(),"input.length()="+input.length());
int index;
String str;
String strChanged;
StringBuilder strBuilder = new StringBuilder();
for(index=0;index<input.length();index++){
str = input.substring(index,index+1);
strChanged = changeOneWord(str);
strBuilder.append(strChanged);
}
output=strBuilder.toString();
LogUtil.logWithMethod(new Exception(),"after changeAllWord: output="+output);
} catch (Exception e){
e.printStackTrace();
}
return output;
}
其中调用了多个函数,下面分别来说明。
首先是两个基本转换,一个是changeWordProcessSignal(),去掉一些特殊符号,这里的几个符号是我在实测过程中遇到的。如果你测试过程遇到其他不是目标字符集中的符合,也应该在这里去掉。
还有一个函数changeWordProcessEnglish(),是将字母转换为大写,这也是由于我的目标字符集中并没有英文字母,我只是需要获取到字母的对应拼音,并不关心其大小写。所以,是否做这个转换,最终是需要根据自己的目标字符集来决定的。
public String changeWordProcessSignal(String strInput){
String strOutput = strInput;
//去掉 ,。空格-
strOutput = strOutput.replace(",", "");
strOutput = strOutput.replace("。", "");
strOutput = strOutput.replace("-", "");
strOutput = strOutput.replace(" ", "");
return strOutput;
}
public String changeWordProcessEnglish(String strInput){
String strOutput = strInput;
//转大写
strOutput = strOutput.toUpperCase();
return strOutput;
}
接下来,看看for循环里面调用的changeOneWord()函数,这个函数实现对一个字符进行模糊匹配的转换:
//尾字如果是汉字,进行拼音相同字的替换(零不能替换,可以先转换为0)
public String changeOneWord(String strInput){
//若已经在目标集合中了,就不需要转换了
if(numberString.contains(strInput)||numberStringArabic.contains(strInput)){
LogUtil.logWithMethod(new Exception(),"is number");
return strInput;
} else if(specialHanziString.contains(strInput)){
LogUtil.logWithMethod(new Exception(),"is specialHanziString");
return strInput;
}
String strChanged;
List<String> listEnglishPinYin = new ArrayList<String>();
strChanged = changeWord(strInput, numberPinYin, numberString);
if(numberString.contains(strChanged)){
LogUtil.logWithMethod(new Exception(),"is number");
return strChanged;
}
return changeWord(strInput, specialHanziPinYin, specialHanziString);
}
逻辑还是比较清楚的,就是若已经包含着目标字符集中,直接返回。若没有包含,则进行拼音的相似转换,然后返回。
拼音的相似转换,是在changeWord()中实现的。在这里,先分析字符类型,获取拼音,然后与目标进行对比。
changeWord()中,先判断输入,是什么类型的字符:数字、字母、汉字:
private String changeWord(String strInput, List<String> listPinYin, String strSource) {
//先判断输入,是什么类型的字符:数字、字母、汉字
String strOutput="";
String str=strInput.substring(0,1);
String strPinyin = "";
boolean flagGetPinyin=false;
try{
if(str.matches("^[A-Z]{1}$")){
strPinyin = englishPinYin26[englishString26.indexOf(str)];
LogUtil.logWithMethod(new Exception(), "str="+str+" Pinyin="+strPinyin );
flagGetPinyin = true;
}
else if(str.matches("^[0-9]{1}$")){
strPinyin = numberPinYin.get(numberString.indexOf(str));
LogUtil.logWithMethod(new Exception(), "str="+str+" Pinyin="+strPinyin );
flagGetPinyin = true;
}
else if(str.matches("^[\u4e00-\u9fa5]{1}$")){
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITH_TONE_NUMBER);
char c = str.charAt(0);
String[] vals = PinyinHelper.toHanyuPinyinStringArray(c, format);
strPinyin=vals[0];//token.target;
flagGetPinyin = true;
}
然后对拼音进行匹配分析,先是对带音标的拼音进行判断,做相似替换:
if(flagGetPinyin){
//在目标拼音集合中查找匹配项
int num=listPinYin.indexOf(strPinyin);
if(num>=0){ //拼音精确匹配成功
return strSource.substring(num, num+1);
} else {
if(fuzzyMatching){//若开启了模糊匹配
//声母替换
String strPinyinFuzzy = new String(strPinyin) ;//避免修改原字符串
strPinyinFuzzy = replaceHeadString(strPinyinFuzzy);
boolean flagReplacedHeadString = (strPinyinFuzzy==null)?false:true;
if(flagReplacedHeadString){
num=listPinYin.indexOf(strPinyinFuzzy);
if(num>=0){ //拼音模糊匹配成功
LogUtil.logWithMethod(new Exception(), "fuzzy match: "+strPinyinFuzzy+" num="+num);
return strSource.substring(num, num+1);
}
}
//韵母替换
strPinyinFuzzy = new String(strPinyin) ;//避免修改原字符串,不使用声母替换后的字符串
strPinyinFuzzy = replaceTailString(strPinyinFuzzy);
boolean flagReplacedTailString = (strPinyinFuzzy==null)?false:true;
if(flagReplacedTailString){
num=listPinYin.indexOf(strPinyinFuzzy);
if(num>=0){ //拼音模糊匹配成功
LogUtil.logWithMethod(new Exception(), "fuzzy match: "+strPinyinFuzzy+" num="+num);
return strSource.substring(num, num+1);
}
}
//声母韵母都替换
if(flagReplacedHeadString && flagReplacedTailString){
strPinyinFuzzy = replaceHeadString(strPinyinFuzzy);
num=listPinYin.indexOf(strPinyinFuzzy);
if(num>=0){ //拼音模糊匹配成功
LogUtil.logWithMethod(new Exception(), "fuzzy match: "+strPinyinFuzzy+" num="+num);
return strSource.substring(num, num+1);
}
}
然后对不带音标的拼音进行判断,做相似替换:
strPinyin=strPinyin.substring(0, strPinyin.length()-1);
strPinyinFuzzy = new String(strPinyin) ;//避免修改原字符串
num=findPinyin(strPinyinFuzzy,listPinYin);
if(num>=0){ //拼音模糊匹配成功
return strSource.substring(num, num+1);
}
//声母替换
strPinyinFuzzy = replaceHeadString(strPinyinFuzzy);
flagReplacedHeadString = (strPinyinFuzzy==null)?false:true;
if(flagReplacedHeadString){
num=findPinyin(strPinyinFuzzy,listPinYin);
if(num>=0){ //拼音模糊匹配成功
return strSource.substring(num, num+1);
}
}
//韵母替换
strPinyinFuzzy = new String(strPinyin) ;//避免修改原字符串,不使用声母替换后的字符串
strPinyinFuzzy = replaceTailString(strPinyinFuzzy);
flagReplacedTailString = (strPinyinFuzzy==null)?false:true;
if(flagReplacedTailString){
num=findPinyin(strPinyinFuzzy,listPinYin);
if(num>=0){ //拼音模糊匹配成功
return strSource.substring(num, num+1);
}
}
//声母韵母都替换
if(flagReplacedHeadString && flagReplacedTailString){
strPinyinFuzzy = replaceHeadString(strPinyinFuzzy);
num=findPinyin(strPinyinFuzzy,listPinYin);
if(num>=0){ //拼音模糊匹配成功
LogUtil.logWithMethod(new Exception(), "fuzzy match: "+strPinyinFuzzy+" num="+num);
return strSource.substring(num, num+1);
}
}
return str;
} else {
return str;
}
}
} else {//若该字符没有找到相应拼音,使用原字符
strOutput = strInput;
}
} catch (Exception e){
e.printStackTrace();
}
return strOutput;
}
上面的功能实现过程中,还调用了几个函数,例如replaceHeadString()进行声母的近似替换:
private String replaceHeadString(String strPinyin){
//声母替换
String strReplaced = null;
if(strPinyin.contains("ZH")){
strReplaced = strPinyin.replace("ZH", "Z");
} else if(strPinyin.contains("CH")){
strReplaced = strPinyin.replace("CH", "C");
} else if(strPinyin.contains("SH")){
strReplaced = strPinyin.replace("SH", "S");
}
else if(strPinyin.contains("Z")){
strReplaced = strPinyin.replace("Z", "ZH");
} else if(strPinyin.contains("C")){
strReplaced = strPinyin.replace("C", "CH");
} else if(strPinyin.contains("S")){
strReplaced = strPinyin.replace("S", "SH");
}
else if(strPinyin.contains("L")){
strReplaced = strPinyin.replace("L", "N");
} else if(strPinyin.indexOf('N')==0){ //n有在后面的,n只在做声母时易混
strReplaced = strPinyin.replace("N", "L");
} else {
return null;
}
LogUtil.logWithMethod(new Exception(),"strReplaced="+strReplaced);
return strReplaced;//flagReplaced;
}
replaceTailString()进行韵母的近似替换:
private String replaceTailString(String strPinyin) {
// 韵母替换
String strReplaced = null;
if (strPinyin.contains("ANG")) {
strReplaced = strPinyin.replace("ANG", "AN");
} else if (strPinyin.contains("ENG")) {
strReplaced = strPinyin.replace("ENG", "EN");
} else if (strPinyin.contains("ING")) {
strReplaced = strPinyin.replace("ING", "IN");
} else if (strPinyin.contains("AN")) {
strReplaced = strPinyin.replace("AN", "ANG");
} else if (strPinyin.contains("EN")) {
strReplaced = strPinyin.replace("EN", "ENG");
} else if (strPinyin.contains("IN")) {
strReplaced = strPinyin.replace("IN", "ING");
} else {
return null;
}
return strReplaced;
}
findPinyin()在指定拼音集合中寻找某个不带音标的拼音。
注意,由于查找过程中,会有不包含音标的查找,所以不能简单使用List的indexOf()了,我这里是加了一个拼音字符串长度的判断:
private int findPinyin(String strPinyin, List<String> listPinYin){
int num=0;
//在目标拼音集合中查找匹配项
for(String strTmp:listPinYin){
if(strTmp.contains(strPinyin) && strPinyin.length()==(strTmp.length()-1) ){
return num;
}
num++;
}
return -1;
}
}
八、调用的地方是这样的:
String changeToOurWords(String input){
String output=input;
output = new PinyinSimilarity(true).changeOurWordsWithPinyin(output);
return output;
}
九、展示下界面:
界面中展示了一个例子:
输入字符串:
“二连击三版旧时”
点击按钮“change_word”后,转换为
二年级三班,90.0
点击加号按钮,添加到表格中。
完整demo代码:
http://download.csdn.net/detail/lintax/9708173
参考:
http://blog.csdn.net/zhwycan/article/details/7274863
http://blog.csdn.net/lmj623565791/article/details/23187701
http://www.cnblogs.com/bsping/p/4514471.html
更多推荐
所有评论(0)