segmenter.go

//Go中文分词
package sego

import (
    "bufio"
    "fmt"
    "log"
    "math"
    "os"
    "strconv"
    "strings"
    "unicode"
    "unicode/utf8"
)

const (
    minTokenFrequency = 2 // 仅从字典文件中读取大于等于此频率的分词
)

// 分词器结构体
type Segmenter struct {
    dict *Dictionary
}

// 该结构体用于记录Viterbi算法中某字元处的向前分词跳转信息
type jumper struct {
    minDistance float32
    token       *Token
}

// 返回分词器使用的词典
func (seg *Segmenter) Dictionary() *Dictionary {
    return seg.dict
}

// 从文件中载入词典
//
// 可以载入多个词典文件,文件名用","分隔,排在前面的词典优先载入分词,比如
//     "用户词典.txt,通用词典.txt"
// 当一个分词既出现在用户词典也出现在通用词典中,则优先使用用户词典。
//
// 词典的格式为(每个分词一行):
//    分词文本 频率 词性
func (seg *Segmenter) LoadDictionary(files string) {
    seg.dict = NewDictionary()
    for _, file := range strings.Split(files, ",") {
        log.Printf("载入sego词典 %s", file)
        dictFile, err := os.Open(file)
        defer dictFile.Close()
        if err != nil {
            log.Fatalf("无法载入字典文件 \"%s\" \n", file)
        }

        reader := bufio.NewReader(dictFile)
        var text string
        var freqText string
        var frequency int
        var pos string

        // 逐行读入分词
        for {
            size, _ := fmt.Fscanln(reader, &text, &freqText, &pos)

            if size == 0 {
                // 文件结束
                break
            } else if size < 2 {
                // 无效行
                continue
            } else if size == 2 {
                // 没有词性标注时设为空字符串
                pos = ""
            }

            // 解析词频
            var err error
            frequency, err = strconv.Atoi(freqText)
            if err != nil {
                continue
            }

            // 过滤频率太小的词
            if frequency < minTokenFrequency {
                continue
            }

            // 将分词添加到字典中
            words := splitTextToWords([]byte(text))
            token := Token{text: words, frequency: frequency, pos: pos}
            seg.dict.addToken(token)
        }
    }

    // 计算每个分词的路径值,路径值含义见Token结构体的注释
    logTotalFrequency := float32(math.Log2(float64(seg.dict.totalFrequency)))
    for i := range seg.dict.tokens {
        token := &seg.dict.tokens[i]
        token.distance = logTotalFrequency - float32(math.Log2(float64(token.frequency)))
    }

    // 对每个分词进行细致划分,用于搜索引擎模式,该模式用法见Token结构体的注释。
    for i := range seg.dict.tokens {
        token := &seg.dict.tokens[i]
        segments := seg.segmentWords(token.text, true)

        // 计算需要添加的子分词数目
        numTokensToAdd := 0
        for iToken := 0; iToken < len(segments); iToken++ {
            if len(segments[iToken].token.text) > 1 {
                // 略去字元长度为一的分词
                // TODO: 这值得进一步推敲,特别是当字典中有英文复合词的时候
                numTokensToAdd++
            }
        }
        token.segments = make([]*Segment, numTokensToAdd)

        // 添加子分词
        iSegmentsToAdd := 0
        for iToken := 0; iToken < len(segments); iToken++ {
            if len(segments[iToken].token.text) > 1 {
                token.segments[iSegmentsToAdd] = &segments[iToken]
                iSegmentsToAdd++
            }
        }
    }

    log.Println("sego词典载入完毕")
}

// 对文本分词
//
// 输入参数:
//    bytes    UTF8文本的字节数组
//
// 输出:
//    []Segment    划分的分词
func (seg *Segmenter) Segment(bytes []byte) []Segment {
    return seg.internalSegment(bytes, false)
}

func (seg *Segmenter) internalSegment(bytes []byte, searchMode bool) []Segment {
    // 处理特殊情况
    if len(bytes) == 0 {
        return []Segment{}
    }

    // 划分字元
    text := splitTextToWords(bytes)

    return seg.segmentWords(text, searchMode)
}

func (seg *Segmenter) segmentWords(text []Text, searchMode bool) []Segment {
    // 搜索模式下该分词已无继续划分可能的情况
    if searchMode && len(text) == 1 {
        return []Segment{}
    }

    // jumpers定义了每个字元处的向前跳转信息,包括这个跳转对应的分词,
    // 以及从文本段开始到该字元的最短路径值
    jumpers := make([]jumper, len(text))

    tokens := make([]*Token, seg.dict.maxTokenLength)
    for current := 0; current < len(text); current++ {
        // 找到前一个字元处的最短路径,以便计算后续路径值
        var baseDistance float32
        if current == 0 {
            // 当本字元在文本首部时,基础距离应该是零
            baseDistance = 0
        } else {
            baseDistance = jumpers[current-1].minDistance
        }

        // 寻找所有以当前字元开头的分词
        numTokens := seg.dict.lookupTokens(
            text[current:minInt(current+seg.dict.maxTokenLength, len(text))], tokens)

        // 对所有可能的分词,更新分词结束字元处的跳转信息
        for iToken := 0; iToken < numTokens; iToken++ {
            location := current + len(tokens[iToken].text) - 1
            if !searchMode || current != 0 || location != len(text)-1 {
                updateJumper(&jumpers[location], baseDistance, tokens[iToken])
            }
        }

        // 当前字元没有对应分词时补加一个伪分词
        if numTokens == 0 || len(tokens[0].text) > 1 {
            updateJumper(&jumpers[current], baseDistance,
                &Token{text: []Text{text[current]}, frequency: 1, distance: 32, pos: "x"})
        }
    }

    // 从后向前扫描第一遍得到需要添加的分词数目
    numSeg := 0
    for index := len(text) - 1; index >= 0; {
        location := index - len(jumpers[index].token.text) + 1
        numSeg++
        index = location - 1
    }

    // 从后向前扫描第二遍添加分词到最终结果
    outputSegments := make([]Segment, numSeg)
    for index := len(text) - 1; index >= 0; {
        location := index - len(jumpers[index].token.text) + 1
        numSeg--
        outputSegments[numSeg].token = jumpers[index].token
        index = location - 1
    }

    // 计算各个分词的字节位置
    bytePosition := 0
    for iSeg := 0; iSeg < len(outputSegments); iSeg++ {
        outputSegments[iSeg].start = bytePosition
        bytePosition += textSliceByteLength(outputSegments[iSeg].token.text)
        outputSegments[iSeg].end = bytePosition
    }
    return outputSegments
}

// 更新跳转信息:
//     1. 当该位置从未被访问过时(jumper.minDistance为零的情况),或者
//    2. 当该位置的当前最短路径大于新的最短路径时
// 将当前位置的最短路径值更新为baseDistance加上新分词的概率
func updateJumper(jumper *jumper, baseDistance float32, token *Token) {
    newDistance := baseDistance + token.distance
    if jumper.minDistance == 0 || jumper.minDistance > newDistance {
        jumper.minDistance = newDistance
        jumper.token = token
    }
}

// 取两整数较小值
func minInt(a, b int) int {
    if a > b {
        return b
    }
    return a
}

// 取两整数较大值
func maxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// 将文本划分成字元
func splitTextToWords(text Text) []Text {
    output := make([]Text, 0, len(text)/3)
    current := 0
    inAlphanumeric := true
    alphanumericStart := 0
    for current < len(text) {
        r, size := utf8.DecodeRune(text[current:])
        if size <= 2 && (unicode.IsLetter(r) || unicode.IsNumber(r)) {
            // 当前是拉丁字母或数字(非中日韩文字)
            if !inAlphanumeric {
                alphanumericStart = current
                inAlphanumeric = true
            }
        } else {
            if inAlphanumeric {
                inAlphanumeric = false
                if current != 0 {
                    output = append(output, toLower(text[alphanumericStart:current]))
                }
            }
            output = append(output, text[current:current+size])
        }
        current += size
    }

    // 处理最后一个字元是英文的情况
    if inAlphanumeric {
        if current != 0 {
            output = append(output, toLower(text[alphanumericStart:current]))
        }
    }

    return output
}

// 将英文词转化为小写
func toLower(text []byte) []byte {
    output := make([]byte, len(text))
    for i, t := range text {
        if t >= 'A' && t <= 'Z' {
            output[i] = t - 'A' + 'a'
        } else {
            output[i] = t
        }
    }
    return output
}

转载于:https://www.cnblogs.com/zhangboyu/p/7462003.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/467786.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我在MTK平台下调试音频ALSA

#前言前言我就随便写了&#xff0c;因为是项目的需要&#xff0c;我需要在我们的MTK8167S平台上面调试音频。包括录音和播放。#硬件原理图因为是我们公司的项目&#xff0c;我就不能把完整的原理图给出来。因为两个MIC不涉及机密&#xff0c;跟MTK的公版是一样的。可以给出来大…

java 左边补0_java 数字左补齐0

NumberFormat nf NumberFormat.getInstance();//设置是否使用分组nf.setGroupingUsed(false);//设置最大整数位数nf.setMaximumIntegerDigits(2);nf.setMinimumIntegerDigits(2);//可以左补齐两位数的数字//以下是查询当前天数的所有日期String nowDaygetNowYMD();String curD…

一切不怕从零开始

不知道大家有没有看过吴京题为<<一切不怕从零开始>>的演讲。我曾经刷微博的时候看到过,印象和触动最深的是他的那句"我走过很多的路,换过很多的方向,不敢说有什么成就,到今天我觉得唯一能够让我拿出来炫耀的可能就是,我不害怕从头开始"。这样的话,对年龄…

60、二叉搜索树的第k个结点

一、题目 给定一颗二叉搜索树&#xff0c;请找出其中的第k大的结点。例如&#xff0c; 5 / \ 3 7 /\ /\ 2 4 6 8 中&#xff0c;按结点数值大小顺序第三个结点的值为4。 二、解法 1 package algorithm7;2 3 public class KthNode62 {4 public static void main(String[] ar…

机器学习算法3

文章目录转换器与估计器分类算法-K近邻算法一个例子弄懂k-近邻计算距离公式sklearn.neighborsMethodk近邻实例k-近邻算法案例分析对Iris数据集进行分割对特征数据进行标准化朴素贝叶斯概率论基础联合概率与条件概率联合概率条件概率如果每个事件相互独立拉普拉斯平滑sklearn朴素…

嵌入式杂谈之文件系统

文件系统可以说是嵌入式中的一大块&#xff0c;也是绕不过的一部分。之前我对文件系统认知一直停留在在U盘格式的理解上&#xff0c;直到接触了嵌入式Linux才发现这里面大有文章&#xff0c;以Linux启动挂载根文件系统为例&#xff0c;这个文件系统可以是真正的存储设备上的文件…

qtscrcpy自定义按键_按键映射说明.md

# 自定义按键映射说明按键映射文件为json格式&#xff0c;新增自己的按键映射文件需要放在keymap目录中才可以被QtScrcpy识别。按键映射文件的具体编写格式下面会介绍&#xff0c;也可以参考自带的按键映射文件。## 按键映射脚本格式说明### 通用说明- 按键映射中的坐标位置都是…

docker发布spring cloud应用

原文地址&#xff1a;http://www.cnblogs.com/skyblog/p/5163691.html 本文涉及到的项目&#xff1a; cloud-simple-docker&#xff1a;一个简单的spring boot应用 Docker是一种虚拟机技术&#xff0c;准确的说是在linux虚拟机技术LXC基础上又封装了一层&#xff0c;可以看成是…

Linux块设备IO子系统

块设备是Linux三大设备之一&#xff0c;其驱动模型主要针对磁盘&#xff0c;Flash等存储类设备&#xff0c;块设备&#xff08;blockdevice&#xff09;是一种具有一定结构的随机存取设备&#xff0c;对这种设备的读写是按块(所以叫块设备)进行的&#xff0c;他使用缓冲区来存放…

机器学习算法4

文章目录精确率和召回率分类器性能评估混淆矩阵分类模型sklearn.metrics.classification_report决策树认识决策树优缺点信息的度量和作用信息熵决策树划分依据之信息增益api实战method决策树优缺点分析实例随机森林集成方法&#xff08;分类&#xff09;之随机森林学习算法skle…

unity怎么制作云飘动_Unity 如何制作星空粒子效果?

本经验介绍在Unity游戏引擎中使用Particle System&#xff0c;如何使用自带资源&#xff0c;快速制作星空的粒子特效。工具/原料Unity方法/步骤1:首先&#xff0c;新建一个场景&#xff0c;如果有自己的天空盒资源的话&#xff0c;在Window->Lighting下设置下天空(默认天空盒…

[芦半山]Android native分析工具ASAN和HWASAN原理解析

ASAN和HWASAN原理解析由于虚拟机的存在&#xff0c;Android应用开发者们通常不用考虑内存访问相关的错误。而一旦我们深入到Native世界中&#xff0c;原本面容和善的内存便开始凶恶起来。这时&#xff0c;由于程序员写法不规范、逻辑疏漏而导致的内存错误会统统跳到我们面前&am…

线性回归,岭回归

文章目录线性回归回归算法回归算法之线性回归优缺点损失函数梯度下降算法LinearRegression属性加入交叉验证线性回归案例分析波士顿房价预测5.性能评测案例欠拟合与过拟合解决过拟合的方法回归算法之岭回归sklearn.linear_model.Ridge方法属性案例分析线性回归 回归算法 回归…

resnet152训练_Resnet-152的图像预处理

152层的 Resnet的图片输入尺寸为224*224&#xff0c;那对于大多数情况&#xff0c;图片的分辨率都是大于这个数值&#xff0c;那么该如何把图片的尺寸裁剪到这样一个尺寸&#xff0c;又如何进行数据增强呢&#xff1f;第一&#xff0c;调整尺寸(Rescaling)先将图片较短的那条边…

重处理报表的坑

1.创建报表时&#xff0c;会记录符合条件的FileID 2.重处理报表时&#xff0c;不会根据条件去找符合条件的数据&#xff1b;而是根据原先的FileID去生成&#xff1b; 意味着新增了数据&#xff0c;原先的报表重处理也增加不了数据...必须要重新生成报表 转载于:https://www.cnb…

C 语言中,x += 5 == 4 是什么意思?

#讨论这个有意义吗&#xff1f;这个是在知乎上看到的一个问题&#xff0c;评论挺多的。其中有人提到&#xff0c;研究这个东西有什么用&#xff1f;编程的时候我们不能这么写的。我记得在大学的时候&#xff0c;我们的副院长给我们上课&#xff0c;就给我们提到&#xff0c;要习…

UEditor编辑器第一次赋值失败的解决方法

网上查了很多方式都不是很好用&#xff0c;最后想到了这样的处理方式 首先在js中定义一个全局变量 var ue null; 然后在初始化显示编辑器的时候js这样写 if (ue null) { ue new baidu.editor.ui.Editor(); ue.render(email_template); } ue.setContent("11111");…

linux命令deploy_Linux deploy 使用教程

Linux deploy是一个可以快速在Android设备上安装运行Linux操作系统的App&#xff0c;遵循GPLv3协议。我已经用了一段时间&#xff0c;讲讲我的使用经验&#xff0c;以下Linux deploy我简写为Ld。首先下载安装Ld&#xff0c;运行Ld后进入了一个全黑的界面&#xff0c;其实是个自…

一次深刻的面试经历

没有吐槽&#xff0c;没有埋怨&#xff0c;没有鸡汤&#xff0c;纯分享。近期我到某名牌房地产公司参加了一次面试&#xff0c;面试岗位是企划主管&#xff0c;我把面试经历跟大家简单分享一下。面试背景&#xff1a;我一直从事广告传媒工作&#xff0c;在工作中服务过不同的客…

帆软报表嵌入python程序_在线报表FineReport中如何进行嵌入式部署

2.部分复制也可以选择性的只复制必要性文件至已有工程中。必须复制的文件fr-server-7.1.jar&#xff1a;包含了报表服务的所有功能&#xff0c;必须拷贝至WEB-INF\lib下&#xff1b;fr-third-7.1.jar&#xff1a;包含了报表服务引用的第三方插件&#xff0c;必须拷贝至WEB-INF\…