一、语音识别库的问题

最近在做语音识别,使用了科大讯飞的语音识别库。
首先得说,这个识别率挺高的。
可是终究还是有时会出错,特别是一些易混音,例如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

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐