拍照搜题功能服务端(php)开发——simhash

时间:2019-02-16
本文章向大家介绍拍照搜题功能服务端(php)开发——simhash,主要包括拍照搜题功能服务端(php)开发——simhash使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

        近期接到的项目中有一个功能——手机拍照搜索试题。首先进行分析,客户端调用ocr接口识别照片中的文字,服务端拿到文字去题库进行搜索。对后端开发的我来说,问题就变为如何根据关键词搜索匹配度较高的题目。一个全新的问题,之前没有接触过,此时内心有些崩溃,不知道能不能实现,而且要给出开发工期,要怎么办?姑且先给5天吧,如果调研失败,我有最后的保留方案——用mysql全文索引实现(在类似需求中应用过,有局限性,不是根据语义分析而是根据标点拆分文章,看句子是否在题目中出现过)。

       调研开始发现百度App有拍照搜题的功能,既然人家能实现,说明这个问题是有解的,进一步增强了自己的信息。

       后来发现很多关于simHash的文章,其中一篇:simhash算法原理及实现。用简单的话解释一下simhash:这个算法可以把任何文章都哈希成一个64位的二进制码,语义越相近的文章,获得的二进制码的汉明距离越小。如果感兴趣,大家可以深入研究一下。

      基于这个理论,经过大量尝试(其中不乏开源代码,在此非常感谢),终于完成了下面的类。simhash第一步首先要分词,故这个类用到了php的scws这个拓展。

<?php
class Glo_Simhash {
    /**
     * 全角字符转变成半角字符
     * @param $str
     * @return mixed
     */

    function replace_DBC2SBC($str) {
        $DBC = Array(
            '0' , '1' , '2' , '3' , '4' ,
            '5' , '6' , '7' , '8' , '9' ,
            'A' , 'B' , 'C' , 'D' , 'E' ,
            'F' , 'G' , 'H' , 'I' , 'J' ,
            'K' , 'L' , 'M' , 'N' , 'O' ,
            'P' , 'Q' , 'R' , 'S' , 'T' ,
            'U' , 'V' , 'W' , 'X' , 'Y' ,
            'Z' , 'a' , 'b' , 'c' , 'd' ,
            'e' , 'f' , 'g' , 'h' , 'i' ,
            'j' , 'k' , 'l' , 'm' , 'n' ,
            'o' , 'p' , 'q' , 'r' , 's' ,
            't' , 'u' , 'v' , 'w' , 'x' ,
            'y' , 'z' , '-' , ' ' , ':' ,
            '。' , ',' , '/' , '%' , '#' ,
            '!' , '@' , '&' , '(' , ')' ,
            '<' , '>' , '"' , ''' , '?' ,
            '[' , ']' , '{' , '}' , '\' ,
            '|' , '+' , '=' , '_' , '^' ,
            '¥' , ' ̄' , '`' , '“' , '”',
            ';' , '·'
        );
        $SBC = Array(
            '0', '1', '2', '3', '4',
            '5', '6', '7', '8', '9',
            'A', 'B', 'C', 'D', 'E',
            'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O',
            'P', 'Q', 'R', 'S', 'T',
            'U', 'V', 'W', 'X', 'Y',
            'Z', 'a', 'b', 'c', 'd',
            'e', 'f', 'g', 'h', 'i',
            'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's',
            't', 'u', 'v', 'w', 'x',
            'y', 'z', '-', ' ', ':',
            '.', ',', '/', '%', '#',
            '!', '@', '&', '(', ')',
            '<', '>', '"', '\'','?',
            '[', ']', '{', '}', '\\',
            '|', '+', '=', '_', '^',
            '$', '~', '`', '"', '"',
            ';', '.'
        );
        return str_replace($DBC, $SBC, $str);
    }

    function hashCode($str) {
        if(empty($str)) return '';
            $mdv = md5($str);
            $mdv1 = substr($mdv,0,16);
            $mdv2 = substr($mdv,16,16);
            $crc1 = abs(crc32($mdv1));
            $crc2 = abs(crc32($mdv2));
            $code =  decbin(bcmul($crc1,$crc2));
            $code = str_repeat('0',64 - strlen($code)).$code;
        return $code;
    }

    function hashCode64($str) {
        $len = 8;
        $md5 = substr(md5($str), 0, $len);
        $seed = 31; 
        $hash = 0;
        for($i = 0; $i < $len; $i++) {  
            $hash = $hash*$seed+ord($md5{$i});
        }
        $hash = $hash & 0x7FFFFFFF;
        $hash = decbin(bcmul($hash,$hash));

        $hash = str_repeat('0',64 - strlen($hash)).$hash;
        return $hash;
    }
    
    //采用scws分词
    function getSimHash($text){
        $so = scws_new();
        $so->set_charset('utf8'); //编码
        $so->set_duality(0);  //散字二元
        $so->set_ignore(0); //忽略标点符号
        $so->set_multi(0);
        $str = $this->replace_DBC2SBC($text);
        //过滤字符
        $filter = [',','.','-','_','`','、','"',"'",":",';','<','>','{','}','(',')'];
        $so->send_text($str);
        $keyList = array();
        while($words = $so->get_result())
        {
            foreach($words as $word){
                $s = $word['word'];
                $weight = intval($word['idf'])*20;
                if(!in_array($s,$filter)  && $weight){
                    $hash = array();
                    $hash_code = $this->hashCode64($s);
                    for($i=0;$i<64;$i++){
                        $value = intval(substr($hash_code,$i,1));
                        if($value==1){
                            $hash[] = $weight;
                        }else{
                            $hash[] = -$weight;
                        }
                    }

                    $keyList[] = $hash;
                }
            }
        }
        $finCode = '';
        for($i=0;$i<64;$i++) {
           $code = 0;
           if(empty($keyList)) {
                break;
           }
           foreach($keyList as $key) { 
                $code+=intval($key[$i]);
           }
           if($code>0) {
                $finCode .= '1';
           } else {
                $finCode .= '0';
           }
        }
        return $finCode;
    }
}

创建倒排索引数据表

CREATE TABLE `simhash_syintelligence` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `hashcode16` char(16) NOT NULL DEFAULT '' COMMENT '16位2进制hash',
  `dp` text NOT NULL  COMMENT '倒排字段 内容:(题目id:hashcode位置;题目id:hashcode位置)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

接下来,如何实现就非常明了了。现将接收到的关键词做一次simHash,然后将值分隔成4段,去表中分4次查询,找到位置对应的题目id即可。

以上是猜想,基于ocr准确识别的情况,但联调时发现,照片识别不是很准确,经常出现类似“问”识别为“间”的情况。simHash是基于语义分析的,我上面的方案必须保证分割的4块中其中一块完全相同,错一字,汉明距离就变大好多,导致可能匹配不到。还有一种情况是,会返回毫不相关的文章,因为这样做返回文章的汉明距离为0-48,于是又改变了策略,调整表的设计:

CREATE TABLE `simhash_syintelligence` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `ques_id` INT(10) NOT NULL DEFAULT '0' COMMENT '题目id',
  `hashcode64` CHAR(64) NOT NULL DEFAULT '' COMMENT '64位2进制hash',
  PRIMARY KEY (`id`),
  KEY `hashcode64` (`hashcode64`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

将题目内容完整hash记录下来,不再做切割。拿到关键词,先做一次simHash,然后轮询表,两两比较汉明距离,如果汉明距离小于某个特定值,则将题目返回回来。

做到现在功能是实现了,但是性能方面还未做测试。待续。