GitVP开源文摘
全部文章/人工智能

LLMs 相关知识及面试题

主要记录大语言大模型(LLMs) 算法(应用)工程师相关的知识及面试题

作者wdndev 仓库wdndev/llm_interview_note ↗ 星标★ 15,185 字数50,660 阅读1
GitHub 原文 ↗
摘要本仓库为大模型面试相关概念,由本人参考网络资源整理,欢迎阅读,如果对你有用,麻烦点一下 `🌟 star`,谢谢!

LLMs 相关知识及面试题

简介

本仓库为大模型面试相关概念,由本人参考网络资源整理,欢迎阅读,如果对你有用,麻烦点一下 🌟 star,谢谢!

为了在低资源情况下,学习大模型,进行动手实践,创建 tiny-llm-zh仓库,旨在构建一个小参数量的中文大语言模型,该项目已部署,可以在如下网站上体验:ModeScope Tiny LLM。

动手实践项目:

  • tiny-llm-zh : 从零实现一个小参数量的中文大语言模型,快速掌握大模型预训练、微调、RL等相关技术;
  • tiny-rag : 实现一个简单的RAG系统,支持多路召回、重排等功能,快速了解搜索相关内容;
  • tiny-mcp : 使用 Prompt 和 Function Calling 实现 MCP (模型上下文协议)服务端和客户端,快速使用MCP搭建Agent项目。
  • llama3-from-scratch-zh : 从零实现 llama3, 可加载 meta 官方权重,可在本地笔记本(16G内存)调试运行

其他学习资源推荐:

  • AI 工程师八股 : 包含深度学习、机器学习、推荐系统、搜索系统等通用知识

在线阅读

在线阅读链接:LLMs Interview Note

注意:

相关答案为自己撰写,若有不合理地方,请指出修正,谢谢!

欢迎关注微信公众号,会不定期更新LLM内容,以及一些面试经验:

weixin

目录

* [1.语言模型](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/1.语言模型/1.语言模型.md)
* [1.分词](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/1.分词/1.分词.md)
* [2.jieba分词用法及原理](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/2.jieba分词用法及原理/2.jieba分词用法及原理.md)
* [3.词性标注](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/3.词性标注/3.词性标注.md)
* [4.句法分析](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/4.句法分析/4.句法分析.md)
* [5.词向量](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/5.词向量/5.词向量.md)
* [Word2Vec](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/Word2Vec/Word2Vec.md)
* [NLP三大特征抽取器(CNN/RNN/TF)](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/NLP三大特征抽取器(CNN-RNN-TF)/NLP三大特征抽取器(CNN-RNN-TF).md)
* [NLP面试题](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/NLP面试题/NLP面试题.md)
* [LLM为什么Decoder only架构](</01.大语言模型基础/LLM为什么Decoder only架构/LLM为什么Decoder only架构.md> "LLM为什么Decoder only架构")
* [1.激活函数](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/1.激活函数/1.激活函数.md)
* [1.llm概念](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/01.大语言模型基础/1.llm概念/1.llm概念.md)
* [1.attention](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/1.attention/1.attention.md)
* [2.layer\_normalization](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/2.layer_normalization/2.layer_normalization.md)
* [3.位置编码](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/3.位置编码/3.位置编码.md)
* [4.tokenize分词](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/4.tokenize分词/4.tokenize分词.md)
* [5.token及模型参数](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/5.token及模型参数/5.token及模型参数.md)
* [6.激活函数](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/6.激活函数/6.激活函数.md)
* [MHA\_MQA\_GQA](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/MHA_MQA_GQA/MHA_MQA_GQA.md)
* [解码策略(Top-k & Top-p & Temperature)](</02.大语言模型架构/解码策略(Top-k & Top-p & Temperatu/解码策略(Top-k & Top-p & Temperature).md> "解码策略(Top-k & Top-p & Temperature)")
* [bert细节](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/bert细节/bert细节.md)
* [Transformer架构细节](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/Transformer架构细节/Transformer架构细节.md)
* [bert变种](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/bert变种/bert变种.md)
* [llama系列模型](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/llama系列模型/llama系列模型.md)
* [chatglm系列模型](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/chatglm系列模型/chatglm系列模型.md)
* [llama 2代码详解](</02.大语言模型架构/llama 2代码详解/llama 2代码详解.md> "llama 2代码详解")
* [llama 3](</02.大语言模型架构/llama 3/llama 3.md> "llama 3")
* [1.MoE论文](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/1.MoE论文/1.MoE论文.md)
* [2.MoE经典论文简牍](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/02.大语言模型架构/2.MoE经典论文简牍/2.MoE经典论文简牍.md)
* [3.LLM MoE :Switch Transformers](</02.大语言模型架构/3.LLM MoE :Switch Transformers/3.LLM MoE :Switch Transformers.md> "3.LLM MoE :Switch Transformers")
* [数据格式](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/03.训练数据集/数据格式/数据格式.md)
* [1.概述](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/1.概述/1.概述.md)
* [2.数据并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/2.数据并行/2.数据并行.md)
* [3.流水线并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/3.流水线并行/3.流水线并行.md)
* [4.张量并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/4.张量并行/4.张量并行.md)
* [5.序列并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/5.序列并行/5.序列并行.md)
* [6.多维度混合并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/6.多维度混合并行/6.多维度混合并行.md)
* [7.自动并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/7.自动并行/7.自动并行.md)
* [8.moe并行](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/8.moe并行/8.moe并行.md)
* [9.总结](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/9.总结/9.总结.md)
* [deepspeed介绍](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/deepspeed介绍/deepspeed介绍.md)
* [1.分布式训练题目](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/分布式训练题目/分布式训练题目.md)
* [2.显存问题](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/04.分布式训练/1.显存问题/1.显存问题.md)
* [1.基本概念](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/1.基本概念/1.基本概念.md)
* [2.prompting](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/2.prompting/2.prompting.md)
* [3.adapter-tuning](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/3.adapter-tuning/3.adapter-tuning.md)
* [4.lora](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/4.lora/4.lora.md)
* [5.总结](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/5.总结/5.总结.md)
* [llama2微调](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/llama2微调/llama2微调.md)
* [ChatGLM3微调](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/ChatGLM3微调/ChatGLM3微调.md)
* [1.微调](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/1.微调/1.微调.md)
* [2.预训练](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/05.有监督微调/2.预训练/2.预训练.md)
* [0.llm推理框架简单总结](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/0.llm推理框架简单总结/0.llm推理框架简单总结.md)
* [1.vllm](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/1.vllm/1.vllm.md)
* [2.text_generation\_inference](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/2.text_generation_inference/2.text_generation_inference.md)
* [3.faster_transformer](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/3.faster_transformer/3.faster_transformer.md)
* [4.trt_llm](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/4.trt_llm/4.trt_llm.md)
* [llm推理优化技术](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/llm推理优化技术/llm推理优化技术.md)
* [1.推理](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/06.推理/1.推理/1.推理.md)
* [策略梯度(pg)](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/策略梯度(pg)/策略梯度(pg).md)
* [近端策略优化(ppo)](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/近端策略优化(ppo)/近端策略优化(ppo).md "近端策略优化(ppo)")
* [大模型RLHF:PPO原理与源码解读](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/大模型RLHF:PPO原理与源码解读/大模型RLHF:PPO原理与源码解读.md)
* [DPO](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/DPO/DPO.md)
* [1.rlhf相关](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/1.rlhf相关/1.rlhf相关.md)
* [2.强化学习](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/07.强化学习/2.强化学习/2.强化学习.md)
* [检索增强llm](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/08.检索增强rag/检索增强llm/检索增强llm.md)
* [rag(检索增强生成)技术](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/08.检索增强rag/rag(检索增强生成)技术/rag(检索增强生成)技术.md)
* [大模型agent技术](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/08.检索增强rag/大模型agent技术/大模型agent技术.md)
* [1.评测](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/09.大语言模型评估/1.评测/1.评测.md)
* [1.大模型幻觉](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/09.大语言模型评估/1.大模型幻觉/1.大模型幻觉.md)
* [2.幻觉来源与缓解](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/09.大语言模型评估/2.幻觉来源与缓解/2.幻觉来源与缓解.md)
* [1.思维链(cot)](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/10.大语言模型应用/1.思维链(cot)/1.思维链(cot).md)
* [1.langchain](https://raw.githubusercontent.com/wdndev/llm_interview_note/HEAD/10.大语言模型应用/1.langchain/1.langchain.md)

分词

1.分词

1.概述

分词是自然语言处理的基础,分词准确度直接决定了后面的词性标注、句法分析、词向量以及文本分析的质量。英文语句使用空格将单词进行分隔,除了某些特定词,如how many,New York等外,大部分情况下不需要考虑分词问题。但中文不同,天然缺少分隔符,需要读者自行分词和断句。故在做中文自然语言处理时,需要先进行分词。

2.中文分词难点

中文分词不像英文那样,天然有空格作为分隔。而且中文词语组合繁多,分词很容易产生歧义。因此中文分词一直以来都是NLP的一个重点,也是一个难点。难点主要集中在分词标准,切分歧义和未登录词三部分。

2.1 分词标准

比如人名,有的算法认为姓和名应该分开,有的认为不应该分开。这需要制定一个相对统一的标准。又例如“花草”,有的人认为是一个词,有的人认为应该划分开为两个词“花/草”。某种意义上,中文分词可以说是一个没有明确定义的问题。

2.2 切分歧义

不同的切分结果会有不同的含义,这又包含如下几种情况

  1. 组合型歧义:分词粒度不同导致的不同切分结果。比如“中华人民共和国”,粗粒度的分词结果为“中华人民共和国”,细粒度的分词结果为“中华/人民/共和国”。这种问题需要根据使用场景来选择。在文本分类,情感分析等文本分析场景下,粗粒度划分较好。而在搜索引擎场景下,为了保证recall,细粒度的划分则较好。jieba分词可以根据用户选择的模式,输出粗粒度或者细粒度的分词结果,十分灵活。 另外,有时候汉字串AB中,AB A B可以同时成词,这个时候也容易产生组合型歧义。比如“他/将/来/网商银行”,“他/将来/想/应聘/网商银行”。这需要通过整句话来区分。 组合型歧义描述的是AB A B均可以同时成词的汉字串,它是可以预测的,故也有专家称之为“固有型歧义”
  2. 交集型歧义:不同切分结果共用相同的字,前后组合的不同导致不同的切分结果。比如“南京市长江大桥”,可以划分为“南京市/长江大桥”,也可以划分为“南京/市长/江大桥”。这也需要通过整句话来区分。交集型歧义前后组合,变化很多,难以预测,故也有专家称之为“偶发型歧义”。
  3. 真歧义:本身语法或语义没有问题,即使人工切分也会产生歧义。比如“下雨天留客天天留人不留”,可以划分为“下雨天/留客天/天留/人不留”,也可以划分为“下雨天/留客天/天留人不/留”。此时通过整句话还没法切分,只能通过上下文语境来进行切分。如果是不想留客,则切分为前一个。否则切分为后一个。

有专家统计过,中文文本中的切分歧义出现频次为1.2次/100汉字,其中交集型歧义和组合型歧义占比为12:1。而对于真歧义,一般出现的概率不大。

2.3 未登录词

也叫新词发现,或者生词,未被词典收录的词。未登录词分为如下几种类型

  1. 新出现的词汇,比如一些网络热词,如“超女”“给力”等
  2. 专有名词,主要是人名 地名 组织机构,比如“南苏丹”“特朗普” “花呗”“借呗”等。
  3. 专业名词和研究领域词语,比如“苏丹红” “禽流感”
  4. 其他专有名词,比如新出现的电影名、产品名、书籍名等。

未登录词对于分词精度的影响远远超过歧义切分。未登录词识别难度也很大,主要原因有

  1. 未登录词增长速度往往比词典更新速度快很多,因此很难利用更新词典的方式解决未登录词问题。不过词典越大越全,分词精度也会越高。因此一个大而全的词典还是相当重要的。
  2. 未登录词都是由普通词汇构成,长度不定,也没有明显的边界标志词
  3. 未登录词还有可能与上下文中的其他词汇构成交集型歧义。
  4. 未登录词中还有可能夹杂着英语字母等其他符号,这也带来了很大难度。比如“e租宝”。

对于词典中不包含的未登录词,无法基于字符串匹配来进行识别。此时基于统计的分词算法就可以大显身手了,jieba分词采用了HMM隐马尔科夫模型和viterbi算法来解决未登录词问题。下一篇文章我们会详细分析这个算法过程。

3.中文分词算法

当前的分词算法主要分为两类,基于词典的规则匹配方法,和基于统计的机器学习方法。

3.1 基于词典的分词算法

基于词典的分词算法,本质上就是字符串匹配。将待匹配的字符串基于一定的算法策略,和一个足够大的词典进行字符串匹配,如果匹配命中,则可以分词。根据不同的匹配策略,又分为正向最大匹配法,逆向最大匹配法,双向匹配分词,全切分路径选择等。

最大匹配法主要分为三种:

  1. 正向最大匹配法,从左到右对语句进行匹配,匹配的词越长越好。比如“羽毛球拍卖完了”,可能会优先划分为“羽毛球拍/卖完了”,而不是“羽毛球/拍卖/完了”。这种方式切分会有歧义问题出现,比如“结婚和尚未结婚的同事”,会被划分为“结婚/和尚/未/结婚/的/同事”。
  2. 逆向最大匹配法,从右到左对语句进行匹配,同样也是匹配的词越长越好。比如“他从东经过我家”,划分为“他/从/东/经过/我家”。这种方式同样也会有歧义问题,比如“他们昨日本应该回来”,会被划分为“他们/昨/日本/应该/回来”。
  3. 双向匹配分词,则同时采用正向最大匹配和逆向最大匹配,选择二者分词结果中词数较少者。但这种方式同样会产生歧义问题,比如“他将来上海”,会被划分为“他/将来/上海”。由此可见,词数少也不一定划分就正确。

全切分路径选择,将所有可能的切分结果全部列出来,从中选择最佳的切分路径。分为两种选择方法

  1. n最短路径方法。将所有的切分结果组成有向无环图,切词结果作为节点,词和词之间的边赋予权重,找到权重和最小的路径即为最终结果。比如可以通过词频作为权重,找到一条总词频最大的路径即可认为是最佳路径。
  2. n元语法模型。同样采用n最短路径,只不过路径构成时会考虑词的上下文关系。一元表示考虑词的前后一个词,二元则表示考虑词的前后两个词。然后根据语料库的统计结果,找到概率最大的路径。

3.2 基于统计的分词算法

基于统计的分词算法,本质上是一个序列标注问题。将语句中的字,按照他们在词中的位置进行标注。标注主要有:B(词开始的一个字),E(词最后一个字),M(词中间的字,可能多个),S(一个字表示的词)。例如“网商银行是蚂蚁金服微贷事业部的最重要产品”,标注后结果为“BMMESBMMEBMMMESBMEBE”,对应的分词结果为“网商银行/是/蚂蚁金服/微贷事业部/的/最重要/产品”。

基于统计分析方法,得到序列标注结果,就可以得到分词结果了。这类算法基于机器学习或者现在火热的深度学习,主要有HMM,CRF,SVM,以及深度学习等。

  1. HMM,隐马尔科夫模型。隐马尔科夫模型在机器学习中应用十分广泛,它包含观测序列和隐藏序列两部分。对应到NLP中,语句是观测序列,而序列标注结果是隐藏序列。任何一个HMM都可以由一个五元组来描述:观测序列,隐藏序列,隐藏态起始概率,隐藏态之间转换概率(转移概率),隐藏态表现为观测值的概率(发射概率)。其中起始概率,转移概率和发射概率可以通过大规模语料统计来得到。从隐藏态初始状态出发,计算下一个隐藏态的概率,并依次计算后面所有的隐藏态转移概率。序列标注问题就转化为了求解概率最大的隐藏状态序列问题。jieba分词中使用HMM模型来处理未登录词问题,并利用viterbi算法来计算观测序列(语句)最可能的隐藏序列(BEMS标注序列)。
  2. CRF,条件随机场。也可以描述输入序列和输出序列之间关系。只不过它是基于条件概率来描述模型的。详细的这儿就不展开了。
  3. 深度学习。将语句作为输入,分词结果作为标注,可以进行有监督学习。训练生成模型,从而对未知语句进行预测。

4.分词质量和性能

中文分词对于自然语言处理至关重要,评价一个分词引擎性能的指标主要有分词准确度和分词速度两方面。分词准确度直接影响后续的词性标注,句法分析,文本分析等环节。分词速度则对自然语言处理的实时性影响很大。下图为几种常用分词引擎在准确度和速度方面的对比。

由上可见,想要做准确度很高的通用型分词引擎是多么的困难。如果对准确度要求很高,可以尝试开发特定领域的分词引擎。比如专门针对金融领域。同时从图中可见,作为一款开源的通用型分词引擎,jieba分词的准确度和速度都还是不错的。后面会详细讲解jieba分词的用法及其原理。

5.总结

中文分词是中文自然语言处理中的一个重要环节,为后面的词向量编码,词性标注,句法分析以及文本分析打下了坚实的基础。同时,由于中文缺少空格等分隔符,并且汉字间的组合特别多,很容易产生歧义,这些都加大了中文分词的难度。基于词典的字符串匹配算法和基于统计的分词算法,二者各有优缺点,我们可以考虑结合使用。随着深度学习的兴起,我们可以考虑利用深度学习来进行序列标注和中文分词。


jieba分词用法及原理

2.jieba分词用法及原理

1.概述

上篇文章分析了自然语言处理,特别是中文处理中,分词的几个主要难点。为了解决这些难点,提出了基于字符串匹配的算法和基于统计的分词算法。针对当前的几种分词引擎,对其分词准确度和速度进行了评估。jieba分词作为一个开源项目,在准确度和速度方面均不错,是我们平时常用的分词工具。本文将对jieba分词的使用方法以及原理进行讲解,便于在理解jieba分词原理的同时,加深对前文讲解的分词难点和算法的理解。

1.1 特点

Jieba库分词有4种模式,最常用的还是前3种

  1. 精确模式\\:就是把一段文本精确地切分成若干个中文单词,若干个中文单词之间经过组合,就精确地还原为之前的文本。其中不存在冗余单词 \\。
  2. 全模式\\:将一段文本中所有可能的词语都扫描出来,可能有一段文本它可以切分成不同的模式,或者有不同的角度来切分变成不同的词语,在全模式下,Jieba库会将各种不同的组合都挖掘出来。分词后的信息再组合起来会有冗余,不再是原来的文本 \\。
  3. 搜索引擎模式: 在精确模式基础上,对发现的那些长的词语,我们会对它再次切分,进而适合搜索引擎对短词语的索引和搜索。也有冗余。
  4. paddle模式:利用PaddlePaddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词。同时支持词性标注。paddle模式使用需安装paddlepaddle-tiny,pip install paddlepaddle-tiny==1.6.1。目前paddle模式支持jieba v0.40及以上版本。jieba v0.40以下版本,请升级jieba,pip install jieba --upgrade 。

1.2 安装说明

代码对 Python 2/3 均兼容

  • 全自动安装:easy_install jieba 或者 pip install jieba / pip3 install jieba
  • 半自动安装:先下载 http://pypi.python.org/pypi/jieba/ ,解压后运行 python setup.py install
  • 手动安装:将 jieba 目录放置于当前目录或者 site-packages 目录
  • 通过 import jieba 来引用
  • 如果需要使用paddle模式下的分词和词性标注功能,请先安装paddlepaddle-tiny,pip install paddlepaddle-tiny==1.6.1。

1.3 算法

  • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
  • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
  • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

2.jieba分词用法

jieba分词是一个开源项目,地址为:fxsjy/jieba: 结巴中文分词

它在分词准确度和速度方面均表现不错。其功能和用法如下。

2.1 分词

jieba.cut 方法接受四个输入参数:

  • 需要分词的字符串;
  • cut_all 参数用来控制是否采用全模式;
  • HMM 参数用来控制是否使用 HMM 模型;
  • use_paddle 参数用来控制是否使用paddle模式下的分词模式,paddle模式采用延迟加载方式,通过enable\_paddle接口安装paddlepaddle-tiny,并且import相关代码;

jieba.cut_for_search 方法接受两个参数:

  • 需要分词的字符串;
  • 是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细

待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8

jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用

jieba.lcut 以及 jieba.lcut_for_search 直接返回 list

jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。

支持三种分词模式

# encoding=utf-8
import jieba

seg_list = jieba.cut("我来到北京清华大学", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))  # 全模式

seg_list = jieba.cut("我来到北京清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式

seg_list = jieba.cut("他来到了网易杭研大厦")  # 默认是精确模式
print(", ".join(seg_list))

seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造")  # 搜索引擎模式
print(", ".join(seg_list))

输出为

【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学

【精确模式】: 我/ 来到/ 北京/ 清华大学

【新词识别】:他, 来到, 了, 网易, 杭研, 大厦    (此处,“杭研”并没有在词典中,但是也被Viterbi算法识别出来了)

【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造

2.2 添加自定义词典

  • 开发者可以指定自己自定义的词典,以便包含 jieba 词库里没有的词。虽然 jieba 有新词识别能力,但是自行添加新词可以保证更高的正确率
  • 用法: jieba.load_userdict(file_name) , file\_name 为文件类对象或自定义词典的路径
  • 词典格式和 dict.txt 一样,一个词占一行;每一行分三部分:词语、词频(可省略)、词性(可省略),用空格隔开,顺序不可颠倒。file_name 若为路径或二进制方式打开的文件,则文件必须为 UTF-8 编码。
  • 词频省略时使用自动计算的能保证分出该词的词频。

使用起来很简单,我们先创建一个文件,比如user_dict.txt,其中每一行代表一个新词,分别为词语,词频,词性。如下:

创新办 3 i
云计算 5
凱特琳 nz
台中

然后在代码中分词前,加载这个自定义词典即可。更改分词器(默认为 jieba.dt)的 tmp_dir 和 cache_file 属性,可分别指定缓存文件所在的文件夹及其文件名,用于受限的文件系统。

jieba.load_userdict("user_dict.txt")

加载自定义词典的分词效果:

之前: 李小福 / 是 / 创新 / 办 / 主任 / 也 / 是 / 云 / 计算 / 方面 / 的 / 专家 /

加载自定义词库后: 李小福 / 是 / 创新办 / 主任 / 也 / 是 / 云计算 / 方面 / 的 / 专家 /

2.3 调整词典

  • 使用 add_word(word, freq=None, tag=None) 和 del_word(word) 可在程序中动态修改词典。
  • 使用 suggest_freq(segment, tune=True) 可调节单个词语的词频,使其能(或不能)被分出来。
  • 注意:自动计算的词频在使用 HMM 新词发现功能时可能无效。
# 1 使用del_word()使得某个词语不会出现
print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False)))
如果/放到/post/中将/出错/。

jieba.del_word("中将")
print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False)))
如果/放到/post/中/将/出错/。

# 2 使用add_word()添加新词到字典中
print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False)))
「/台/中/」/正确/应该/不会/被/切开

jieba.add_word("台中")
print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False)))
「/台中/」/正确/应该/不会/被/切开

# 3 使用suggest_freq()调整某个词语的词频,使得其在设置的词频高是能分出,词频低时不能分出
jieba.suggest_freq('台中', True)
69
print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False)))
「/台中/」/正确/应该/不会/被/切开

2.4 关键词提取

关键词提取,将文本中最能表达文本含义的词语抽取出来,有点类似于论文的关键词或者摘要。关键词抽取可以采取:

(1)基于TF-IDF的关键词抽取算法

目标是获取文本中词频高,也就是TF大的,且语料库其他文本中词频低的,也就是IDF大的。这样的词可以作为文本的标志,用来区分其他文本。

API函数

  • jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
    • sentence 为待提取的文本
    • topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20
    • withWeight 为是否一并返回关键词权重值,默认值为 False
    • allowPOS 仅包括指定词性的词,默认值为空,即不筛选
  • jieba.analyse.TFIDF(idf_path=None) ,新建 TFIDF 实例,idf_path 为 IDF 频率文件

代码示例

from jieba import analyse
# 引入TF-IDF关键词抽取接口
tfidf = analyse.extract_tags

# 原始文本
text = "线程是程序执行时的最小单位,它是进程的一个执行流,\
        是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,\
        线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。\
        线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。\
        同样多线程也可以实现并发操作,每个请求分配一个线程来处理。"

# 基于TF-IDF算法进行关键词抽取
keywords = tfidf(text)
print "keywords by tfidf:"
# 输出抽取出的关键词
for keyword in keywords:
    print keyword + "/",


# 输出为:
keywords by tfidf:
线程/ CPU/ 进程/ 调度/ 多线程/ 程序执行/ 每个/ 执行/ 堆栈/ 局部变量/ 单位/ 并发/ 分派/ 一个/ 共享/ 请求/ 最小/ 可以/ 允许/ 分配/ 

(2)基于TextRank的关键词抽取算法

  1. 先将文本进行分词和词性标注,将特定词性的词(比如名词)作为节点添加到图中。
  2. 出现在一个窗口中的词语之间形成一条边,窗口大小可设置为2\~10之间,它表示一个窗口中有多少个词语。
  3. 对节点根据入度节点个数以及入度节点权重进行打分,入度节点越多,且入度节点权重大,则打分高。
  4. 然后根据打分进行降序排列,输出指定个数的关键词。

API函数

  • jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v')) 直接使用,接口相同,注意默认过滤词性。
  • jieba.analyse.TextRank() 新建自定义 TextRank 实例
  • 算法论文: TextRank: Bringing Order into Texts

代码示例

from jieba import analyse
# 引入TextRank关键词抽取接口
textrank = analyse.textrank

# 原始文本
text = "线程是程序执行时的最小单位,它是进程的一个执行流,\
        是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,\
        线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。\
        线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。\
        同样多线程也可以实现并发操作,每个请求分配一个线程来处理。"

print "\nkeywords by textrank:"
# 基于TextRank算法进行关键词抽取
keywords = textrank(text)
# 输出抽取出的关键词
for keyword in keywords:
    print keyword + "/",

# 输出为:
keywords by textrank:
线程/ 进程/ 调度/ 单位/ 操作/ 请求/ 分配/ 允许/ 基本/ 共享/ 并发/ 堆栈/ 独立/ 执行/ 分派/ 组成/ 资源/ 实现/ 运行/ 处理/

2.5 词性标注

利用jieba.posseg模块来进行词性标注,会给出分词后每个词的词性。词性标示兼容ICTCLAS 汉语词性标注集,可查阅网站

API函数

  • jieba.posseg.POSTokenizer(tokenizer=None) 新建自定义分词器,tokenizer 参数可指定内部使用的 jieba.Tokenizer 分词器。jieba.posseg.dt 为默认词性标注分词器。
  • 标注句子分词后每个词的词性,采用和 ictclas 兼容的标记法。
  • 除了jieba默认分词模式,提供paddle模式下的词性标注功能。paddle模式采用延迟加载方式,通过enable_paddle()安装paddlepaddle-tiny,并且import相关代码;

代码示例

import jieba.posseg as pseg
words = pseg.cut("我爱北京天安门")
for word, flag in words:
...    print('%s %s' % (word, flag))
...
我 r        # 代词
爱 v        # 动词
北京 ns        # 名词
天安门 ns        # 名词

paddle模式词性标注对应表如下:

paddle模式词性和专名类别标签集合如下表,其中词性标签 24 个(小写字母),专名类别标签 4 个(大写字母)。

标签含义标签含义标签含义标签含义
n普通名词f方位名词s处所名词t时间
nr人名ns地名nt机构名nw作品名
nz其他专名v普通动词vd动副词vn名动词
a形容词ad副形词an名形词d副词
m数量词q量词r代词p介词
c连词u助词xc其他虚词w标点符号
PER人名LOC地名ORG机构名TIME时间

2.6 并行分词

将文本按行分隔后,每行由一个jieba分词进程处理,之后进行归并处理,输出最终结果。这样可以大大提高分词速度。

原理:将目标文本按行分隔后,把各行文本分配到多个 Python 进程并行分词,然后归并结果,从而获得分词速度的可观提升

基于 python 自带的 multiprocessing 模块,目前暂不支持 Windows

用法:

  • jieba.enable_parallel(4) # 开启并行分词模式,参数为并行进程数
  • jieba.disable_parallel() # 关闭并行分词模式
jieba.enable_parallel(4) # 开启并行分词模式,参数为并行进程数
jieba.disable_parallel() # 关闭并行分词模式

实验结果:在 4 核 3.4GHz Linux 机器上,对金庸全集进行精确分词,获得了 1MB/s 的速度,是单进程版的 3.3 倍。

注意:并行分词仅支持默认分词器 jieba.dt 和 jieba.posseg.dt。

2.7 Tokenize:返回词语在原文的起止位置

注意,输入参数只接受 unicode

默认模式

result = jieba.tokenize(u'永和服装饰品有限公司')
for tk in result:
    print("word %s\t\t start: %d \t\t end:%d" % (tk[0],tk[1],tk[2]))

# 输出为
word 永和                start: 0                end:2
word 服装                start: 2                end:4
word 饰品                start: 4                end:6
word 有限公司            start: 6                end:10

搜索模式

result = jieba.tokenize(u'永和服装饰品有限公司', mode='search')
for tk in result:
    print("word %s\t\t start: %d \t\t end:%d" % (tk[0],tk[1],tk[2]))

# 输出为 
word 永和                start: 0                end:2
word 服装                start: 2                end:4
word 饰品                start: 4                end:6
word 有限                start: 6                end:8
word 公司                start: 8                end:10
word 有限公司            start: 6                end:10   

2.8 命令模式

使用示例:python -m jieba news.txt > cut_result.txt

命令行选项(翻译)

使用: python -m jieba [options] filename

结巴命令行界面。

固定参数:
  filename              输入文件

可选参数:
  -h, --help            显示此帮助信息并退出
  -d [DELIM], --delimiter [DELIM]
                        使用 DELIM 分隔词语,而不是用默认的' / '。
                        若不指定 DELIM,则使用一个空格分隔。
  -p [DELIM], --pos [DELIM]
                        启用词性标注;如果指定 DELIM,词语和词性之间
                        用它分隔,否则用 _ 分隔
  -D DICT, --dict DICT  使用 DICT 代替默认词典
  -u USER_DICT, --user-dict USER_DICT
                        使用 USER_DICT 作为附加词典,与默认词典或自定义词典配合使用
  -a, --cut-all         全模式分词(不支持词性标注)
  -n, --no-hmm          不使用隐含马尔可夫模型
  -q, --quiet           不输出载入信息到 STDERR
  -V, --version         显示版本信息并退出

如果没有指定文件名,则使用标准输入。

--help 选项输出:

$> python -m jieba --help
Jieba command line interface.

positional arguments:
  filename              input file

optional arguments:
  -h, --help            show this help message and exit
  -d [DELIM], --delimiter [DELIM]
                        use DELIM instead of ' / ' for word delimiter; or a
                        space if it is used without DELIM
  -p [DELIM], --pos [DELIM]
                        enable POS tagging; if DELIM is specified, use DELIM
                        instead of '_' for POS delimiter
  -D DICT, --dict DICT  use DICT as dictionary
  -u USER_DICT, --user-dict USER_DICT
                        use USER_DICT together with the default dictionary or
                        DICT (if specified)
  -a, --cut-all         full pattern cutting (ignored with POS tagging)
  -n, --no-hmm          don't use the Hidden Markov Model
  -q, --quiet           don't print loading messages to stderr
  -V, --version         show program's version number and exit

If no filename specified, use STDIN instead.

2.9 延迟加载机制

jieba 采用延迟加载,import jieba 和 jieba.Tokenizer() 不会立即触发词典的加载,一旦有必要才开始加载词典构建前缀字典。如果你想手工初始 jieba,也可以手动初始化。

import jieba
jieba.initialize()  # 手动初始化(可选)

3.jieba分词源码结构

分词的jieba源码版本为0.39。代码结构如下

主要的模块如下

  1. 基本API的封装,在Tokenizer类中,相当于一个外观类。如cut del_word add_word enable_parallel initialize 等
  2. 基于字符串匹配的分词算法,包含一个很大很全的词典,即dict.txt文件
  3. 基于统计的分词算法,实现了HMM隐马尔科夫模型。jieba分词使用了字符串分词和统计分词,结合了二者的优缺点。
  4. 关键词提取,实现了TFIDF和TextRank两种无监督学习算法
  5. 词性标注,实现了HMM隐马尔科夫模型和viterbi算法

4.jieba分词原理分析

jieba分词综合了基于字符串匹配的算法和基于统计的算法,其分词步骤为

  1. 初始化。加载词典文件,获取每个词语和它出现的词数
  2. 切分短语。利用正则,将文本切分为一个个语句,之后对语句进行分词
  3. 构建DAG。通过字符串匹配,构建所有可能的分词情况的有向无环图,也就是DAG
  4. 构建节点最大路径概率,以及结束位置。计算每个汉字节点到语句结尾的所有路径中的最大概率,并记下最大概率时在DAG中对应的该汉字成词的结束位置。
  5. 构建切分组合。根据节点路径,得到词语切分的结果,也就是分词结果。
  6. HMM新词处理:对于新词,也就是dict.txt中没有的词语,通过统计方法来处理,jieba中采用了HMM隐马尔科夫模型来处理。
  7. 返回分词结果:通过yield将上面步骤中切分好的词语逐个返回。yield相对于list,可以节约存储空间。

4.1 初始化

词典是基于字符串匹配的分词算法的关键所在,决定了最终分词的准确度。jieba词典dict.txt是jieba作者采集了超大规模的语料数据,统计得到的。有5M,包含349,046条词语。每一行对应一个词语,包含词语 词数 词性三部分。如下

凤凰寺 22 ns
凤凰山 311 ns
凤凰岭 15 ns
凤凰岭村 2 ns
凤凰木 3 ns

初始化时,先加载词典文件dict.txt,遍历每一行,生成词语-词数的键值对和总词数,并将生成结果保存到cache中,下次直接从cache中读取即可。代码如下,删除了无关的log打印。只需要看关键节点代码即可,不提倡逐行逐行阅读代码,最重要的是理解代码执行的主要流程和关键算法。

def initialize(self, dictionary=None):
    # 获取词典路径
    if dictionary:
        abs_path = _get_abs_path(dictionary)
        if self.dictionary == abs_path and self.initialized:
            return
        else:
            self.dictionary = abs_path
            self.initialized = False
    else:
        abs_path = self.dictionary

    with self.lock:
        try:
            with DICT_WRITING[abs_path]:
                pass
        except KeyError:
            pass
        if self.initialized:
            return

        # 获取cache_file
        default_logger.debug("Building prefix dict from %s ..." % (abs_path or 'the default dictionary'))
        t1 = time.time()
        if self.cache_file:
            cache_file = self.cache_file
        # default dictionary
        elif abs_path == DEFAULT_DICT:
            cache_file = "jieba.cache"
        # custom dictionary
        else:
            cache_file = "jieba.u%s.cache" % md5(
                abs_path.encode('utf-8', 'replace')).hexdigest()
        cache_file = os.path.join(
            self.tmp_dir or tempfile.gettempdir(), cache_file)
        # prevent absolute path in self.cache_file
        tmpdir = os.path.dirname(cache_file)

        # 加载cache_file
        load_from_cache_fail = True
        if os.path.isfile(cache_file) and (abs_path == DEFAULT_DICT or
            os.path.getmtime(cache_file) &gt; os.path.getmtime(abs_path)):
            try:
                with open(cache_file, 'rb') as cf:
                    self.FREQ, self.total = marshal.load(cf)
                load_from_cache_fail = False
            except Exception:
                load_from_cache_fail = True

        # cache_file不存在或者加载失败时,加载原始词典
        if load_from_cache_fail:
            wlock = DICT_WRITING.get(abs_path, threading.RLock())
            DICT_WRITING[abs_path] = wlock
            with wlock:
                # 加载原始词典,得到每个词与其词数的键值对,以及总词数。单个词数除以总词数,即可计算词频
                self.FREQ, self.total = self.gen_pfdict(self.get_dict_file())
                try:
                    # 保存加载的原始词典到cache_file中
                    fd, fpath = tempfile.mkstemp(dir=tmpdir)
                    with os.fdopen(fd, 'wb') as temp_cache_file:
                        marshal.dump(
                            (self.FREQ, self.total), temp_cache_file)
                    _replace_file(fpath, cache_file)
                except Exception:

            try:
                del DICT_WRITING[abs_path]
            except KeyError:
                pass

        self.initialized = True
    

# 加载原始词典
    def gen_pfdict(self, f):
        lfreq = {}
        ltotal = 0
        f_name = resolve_filename(f)

        # 遍历词典每一行,一行包含一个词,词数,以及词性
        for lineno, line in enumerate(f, 1):
            try:
                line = line.strip().decode('utf-8')
                # 取出词语和它的词数
                word, freq = line.split(' ')[:2]
                freq = int(freq)
                # 将词语和它的词数构造成键值对
                lfreq[word] = freq
                # 计算总词数,这个是为了以后计算某个词的词频,词频越大,则改词出现的概率越大
                ltotal += freq
                # 遍历词语中的每个字,如果该字没有出现在词典中,则建立其词语-词数键值对,词数设置为0
                for ch in xrange(len(word)):
                    wfrag = word[:ch + 1]
                    if wfrag not in lfreq:
                        lfreq[wfrag] = 0
            except ValueError:
                raise ValueError(
                    'invalid dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
        f.close()
        # 返回词语-词数的键值对,以及总词数
        return lfreq, ltotal

初始化可以简单理解为,读取词典文件,构建词语-词数键值对,方便后面步骤中查词典,也就是字符串匹配。

4.2. 切分短语

使用汉字正则,切分出连续的汉字和英文字符,形成一段段短语。可以理解为以空格 逗号 句号为分隔,将输入文本切分为一个个短语,之后会基于一个个短语来分词。代码如下

def cut(self, sentence, cut_all=False, HMM=True):
    # 编码转换,utf-8或gbk
    sentence = strdecode(sentence)

    # 根据是否全模式,以及是否采用HMM隐马尔科夫,来设置正则re_han re_skip,以及cut_block
    if cut_all:
        re_han = re_han_cut_all
        re_skip = re_skip_cut_all
    else:
        re_han = re_han_default
        re_skip = re_skip_default
    if cut_all:
        cut_block = self.__cut_all
    elif HMM:
        cut_block = self.__cut_DAG
    else:
        cut_block = self.__cut_DAG_NO_HMM

    # 将输入文本按照空格 逗号 句号等字符进行分割,生成一个个语句子串
    blocks = re_han.split(sentence)

    # 遍历语句子串
    for blk in blocks:
        if not blk:
            continue
        if re_han.match(blk):
            # 对语句进行分词
            for word in cut_block(blk):
                yield word
        else:
            tmp = re_skip.split(blk)
            for x in tmp:
                if re_skip.match(x):
                    yield x
                elif not cut_all:
                    for xx in x:
                        yield xx
                else:
                    yield x
  1. 首先进行将语句转换为UTF-8或者GBK。
  2. 然后根据用户指定的模式,设置cut的真正实现。
  3. 然后根据正则,将输入文本分为一个个语句。
  4. 最后遍历语句,对每个语句单独进行分词。

4.3 构建DAG

下面我们来分析默认模式,也就是精确模式下的分词过程。先来看__cut_DAG方法。

def __cut_DAG(self, sentence):
    # 得到语句的有向无环图DAG
    DAG = self.get_DAG(sentence)
    # 动态规划,计算从语句末尾到语句起始,DAG中每个节点到语句结束位置的最大路径概率,以及概率最大时节点对应词语的结束位置
    route = {}
    self.calc(sentence, DAG, route)
    x = 0
    buf = ''
    N = len(sentence)
    while x &lt; N:
        # y表示词语的结束位置,x为词语的起始位置
        y = route[x][1] + 1
        # 从起始位置x到结束位置y,取出一个词语
        l_word = sentence[x:y]

        if y - x == 1:
            # 单字,一个汉字构成的一个词语
            buf += l_word
        else:
            # 多汉字词语
            if buf:
                if len(buf) == 1:
                    yield buf
                    buf = ''
                else:
                    if not self.FREQ.get(buf):
                        # 词语不在字典中,也就是新词,使用HMM隐马尔科夫模型进行分割
                        recognized = finalseg.cut(buf)
                        for t in recognized:
                            yield t
                    else:
                        for elem in buf:
                            yield elem
                    buf = ''
            yield l_word
        # 该节点取词完毕,跳到下一个词语的开始位置
        x = y

    # 通过yield,逐词返回上一步切分好的词语
    if buf:
        if len(buf) == 1:
            yield buf
        elif not self.FREQ.get(buf):
            recognized = finalseg.cut(buf)
            for t in recognized:
                yield t
        else:
            for elem in buf:
                yield elem

主体步骤如下

  1. 得到语句的有向无环图DAG
  2. 动态规划构建Route,计算从语句末尾到语句起始,DAG中每个节点到语句结束位置的最大路径概率,以及概率最大时节点对应词语的结束位置
  3. 遍历每个节点的Route,组装词语组合。
  4. 如果词语不在字典中,也就是新词,使用HMM隐马尔科夫模型进行分割
  5. 通过yield将词语逐个返回。

下面我们来看构建DAG的过程。先遍历一个个切分好的短语,对这些短语来进行分词。首先要构建短语的有向无环图DAG。查词典进行字符串匹配的过程中,可能会出现好几种可能的切分方式,将这些组合构成有向无环图,如下图所示

可以看到,构成了两条路径:

DAG中记录了某个词的开始位置和它可能的结束位置。开始位置作为key,结束位置是一个list。比如位置0的DAG表达为 {0: [1, 2]}, 也就是说0位置为词的开始位置时,1,2位置都有可能是词的结束位置。上面语句的完整DAG为

{
    0: [1, 2],
    1: [2, 3],
    2: [3],
    3: [4, 5],
    4: [5]
}

DAG构建过程的代码如下:

# 获取语句的有向无环图
def get_DAG(self, sentence):
    self.check_initialized()
    DAG = {}
    N = len(sentence)
    for k in xrange(N):
        tmplist = []
        i = k
        frag = sentence[k]
        while i &lt; N and frag in self.FREQ:
            if self.FREQ[frag]:
                tmplist.append(i)
            i += 1
            frag = sentence[k:i + 1]
        if not tmplist:
            tmplist.append(k)
        DAG[k] = tmplist
    return DAG

4.4 构建节点最大路径概率,以及结束位置

中文一般形容词在前面,而相对来说更关键的名词和动词在后面。考虑到这一点,jieba中对语句,从右向左反向计算路径的最大概率,这个类似于逆向最大匹配。每个词的概率 = 字典中该词的词数 / 字典总词数。对于上图构建每个节点的最大路径概率的过程如下:

p(5)= 1, 
p(4)= max(p(5) * p(4-&gt;5)), 
p(3)= max(p(4) * p(4-&gt;5),   p(5) * p(3-&gt;5)),  # 对于节点3,他有3-&gt;4, 3-&gt;5两条路径,我们取概率最大的路径作为节点3的路径概率,并记下概率最大时节点3的结束位置
p(2) = max(p(3) * p(2-&gt;3))
p(1) = max(p(2) * p(1-&gt;2),   p(3) * p(1-&gt;3))
p(0) = max(p(1) * p(0-&gt;1),   p(2) * p(0-&gt;2))

对应代码如下

def calc(self, sentence, DAG, route):
    N = len(sentence)
    route[N] = (0, 0)
    logtotal = log(self.total)
    for idx in xrange(N - 1, -1, -1):
        # route[idx] = (该汉字到最后一个汉字的最大路径概率, 最大路径概率时该汉字对应的词语结束位置)
        # 遍历DAG中该汉字节点的结束位置,也就是DAG[idx],计算idx到x之间构成的词语的概率,然后乘以x到语句结束位置的最大概率,即可得到idx到语句结束的路径最大概率
        route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])

4.5 构建切分组合

从节点0开始,按照步骤4中构建的最大路径概率以及结束位置,取出节点0的结束位置,构成词语。如果是单字词语,则直接通过yield返回。如果词语在字典中,也直接通过yield返回。如果词语不在字典中,也就是新词,则需要通过HMM隐马尔科夫模型来分割。节点0处理完毕,则跳到下一个词语的开始处进行处理,直至到达语句末尾。

代码参见__cut_DAG(),也就是主体流程代码。

4.6 HMM新词处理

对于新词,也就是dict.txt中没有的词语,通过统计方法来处理,jieba中采用了HMM隐马尔科夫模型。回顾下HMM的五要素:观测序列,隐藏序列,发射概率,起始概率,转移概率。由这五大要素可以对短语建模。

通过语料大规模训练,可以得到发射概率,起始概率和转移概率。通过viterbi算法,可以得到概率最大的隐藏序列,也就是 BEMS标注序列,通过BEMS就可以对语句进行分词了。观察发现,新词被分成二字词语的概率很大。

转移概率在prob_trans.py中,如下

P={'B': {'E': -0.510825623765990, 'M': -0.916290731874155}, # exp后为概率,此处为{'E': 0.6, 'M': 0.4}
 'E': {'B': -0.5897149736854513, 'S': -0.8085250474669937},
 'M': {'E': -0.33344856811948514, 'M': -1.2603623820268226},
 'S': {'B': -0.7211965654669841, 'S': -0.6658631448798212}}

起始概率在prob_start.py中,如下

P={'B': -0.26268660809250016,
 'E': -3.14e+100,
 'M': -3.14e+100,
 'S': -1.4652633398537678}

# exp后为概率,此处为{'B': 0.769, 'E': 0, 'M': 0, 'S': 0.231}

隐马尔科夫模型处理代码主要为

# 通过HMM隐马尔科夫模型获取语句的BEMS序列标注,并通过它来进行分词
def __cut(sentence):
    global emit_P
    # 通过viterbi算法和start_P, trans_P, emit_P三个训练好的概率,得到语句对应的BEMS序列标注
    prob, pos_list = viterbi(sentence, 'BMES', start_P, trans_P, emit_P)
    begin, nexti = 0, 0
    
    # 得到分词结果。根据上面得到pos_list, 也就是语句对应的BEMS序列,来对原始语句进行分词。
    for i, char in enumerate(sentence):
        pos = pos_list[i]
        if pos == 'B':
            # 词语开始
            begin = i
        elif pos == 'E':
            # 词语结束,可以根据begin开始位置来返回分词词语了
            yield sentence[begin:i + 1]
            nexti = i + 1
        elif pos == 'S':
            # 单字词语,直接返回
            yield char
            nexti = i + 1
            
    # 理论上不会走到下面这儿,只是以防万一
    if nexti &lt; len(sentence):
        yield sentence[nexti:]

viterbi算法的代码如下

# 通过viterbi算法,由观测序列,也就是语句,来得到隐藏序列,也就是BEMS标注序列
# obs为语句,states为"BEMS"四种状态,
# start_p为起始概率, trans_p为转移概率, emit_p为发射概率,三者通过语料训练得到
def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]        # 每个汉字的每个BEMS状态的最大概率。
    path = {}       # 分词路径

    # 初始化每个state,states为"BEMS"
    for y in states:
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
        path[y] = [y]

    # 逐字进行处理
    for t in xrange(1, len(obs)):
        V.append({})
        newpath = {}
        # 遍历每个状态
        for y in states:
            # 得到某状态到某个字的发射概率
            em_p = emit_p[y].get(obs[t], MIN_FLOAT)
            # 计算前一个状态到本状态的最大概率和它的前一个状态
            (prob, state) = max(
                [(V[t - 1][y0] + trans_p[y0].get(y, MIN_FLOAT) + em_p, y0) for y0 in PrevStatus[y]])
            # 将该汉字下的某状态(BEMS)的最大概率记下来
            V[t][y] = prob
            # 记录状态转换路径
            newpath[y] = path[state] + [y]
        path = newpath

    # 尝试合并ES两种状态,因为ES经常可以组成一个完整词语
    (prob, state) = max((V[len(obs) - 1][y], y) for y in 'ES')

    # 返回语句的BEMS序列
    return (prob, path[state])

4.7 返回分词结果

通过yield将上面步骤中切分好的词语逐个返回。yield相对于list,可以节约存储空间。

5.总结

jiaba分词是一款十分优秀的开源分词引擎,它结合了基于字符串匹配的算法和基于统计的算法。使用最大概率路径动态规划算法,进行字符串匹配,可以在分词速度快的同时,保持较高的分词精度。使用HMM隐马尔科夫模型对新词进行分词,可以有效解决字符串匹配无法识别新词的难点。阅读它的源码有利于我们加深对分词难点和算法的理解,也能加深对HMM隐马尔卡尔模型这种常用的机器学习算法的理解。


词性标注

3.词性标注

1.概述

词性标注在自然语言处理中也属于基础性的模块,为句法分析、信息抽取等工作打下基础。和分词一样,中文词性标注也存在着很多难点,比如一词多词性,未登录词处理等诸多问题。通过基于字符串匹配的字典查询算法和基于统计的词性标注算法,可以很好的解决这些问题。一般需要先将语句进行分词,然后再进行词性标注。

2.词性标注难点

词性作为词语基本的语法属性,是词语和语句的关键性特征。词性种类也很多,ICTCLAS 汉语词性标注集归纳的词性种类及其表示见 ICTCLAS 汉语词性标注集

词性标注中的难点主要有

  1. 相对于英文,中文缺少词形态变化,不能从词的形态来识别词性
  2. 一词多词性很常见。统计发现,一词多词性的概率高达22.5%。而且越常用的词,多词性现象越严重。比如“研究”既可以是名词(“基础性研究”),也可以是动词(“研究计算机科学”)。
  3. 词性划分标准不统一。词类划分粒度和标记符号等,目前还没有一个广泛认可的统一的标准。比如LDC标注语料中,将汉语一级词性划分为33类,而北京大学语料库则将其划分为26类。词类划分标准和标记符号的不统一,以及分词规范的含糊,都给词性标注带来了很大的困难。jieba分词采用了使用较为广泛的ICTCLAS 汉语词性标注集规范。
  4. 未登录词问题。和分词一样,未登录词的词性也是一个比较大的课题。未登录词不能通过查找字典的方式获取词性,可以采用HMM隐马尔科夫模型等基于统计的算法。

3.词性标注算法

和分词一样,词性标注算法也分为两大类,基于字符串匹配的字典查找算法和基于统计的算法。jieba分词就综合了两种算法,对于分词后识别出来的词语,直接从字典中查找其词性。而对于未登录词,则采用HMM隐马尔科夫模型和viterbi算法来识别。

3.1 基于字符串匹配的字典查找算法

先对语句进行分词,然后从字典中查找每个词语的词性,对其进行标注即可。jieba词性标注中,对于识别出来的词语,就是采用了这种方法。这种方法比较简单,通俗易懂,但是不能解决一词多词性的问题,因此存在一定的误差。

下图即为jieba分词中的词典的一部分词语。每一行对应一个词语,分为三部分,分别为词语名 词数 词性。因此分词完成后只需要在字典中查找该词语的词性即可对其完成标注。

3.2 基于统计的词性标注算法

和分词一样,也可以通过HMM隐马尔科夫模型来进行词性标注。观测序列即为分词后的语句,隐藏序列即为经过标注后的词性标注序列。起始概率 发射概率和转移概率和分词中的含义大同小异,可以通过大规模语料统计得到。观测序列到隐藏序列的计算可以通过viterbi算法,利用统计得到的起始概率 发射概率和转移概率来得到。得到隐藏序列后,就完成了词性标注过程。

4.jieba词性标注原理

jieba在分词的同时,可以进行词性标注。利用jieba.posseg模块来进行词性标注,会给出分词后每个词的词性。词性标示兼容ICTCLAS 汉语词性标注集,可查阅网站

import jieba.posseg as pseg
words = pseg.cut("我爱北京天安门")
for word, flag in words:
...    print('%s %s' % (word, flag))
...
我 r        # 代词
爱 v        # 动词
北京 ns        # 名词
天安门 ns        # 名词

下面来对pseg.cut()进行详细的分析,其主要流程为

  1. 准备工作:check字典是否初始化好,如果没有则先初始化字典。将语句转为UTF-8或者GBK。根据正则匹配,将输入文本分隔成一个个语句。
  2. 遍历语句list,对每个语句进行单独分词和词性标注。
  3. 对于未登录词,使用HMM隐马尔科夫模型处理。

4.1 准备工作

准备工作中做的事情和jieba分词基本一致,check字典是否初始化好,如果没有则先初始化字典。将语句转为UTF-8或者GBK。根据正则匹配,将输入文本分隔成一个个语句。代码如下。

def __cut_internal(self, sentence, HMM=True):
        # 如果没有字典没有初始化,则先加载字典。否则直接使用字典缓存即可。
        self.makesure_userdict_loaded()
        
        # 将语句转为UTF-8或者GBK
        sentence = strdecode(sentence)
        
        # 根据正则匹配,将输入文本分隔成一个个语句。分隔符包括空格 逗号 句号等。
        blocks = re_han_internal.split(sentence)
        
        # 根据是否采用了HMM模型来进行不同方法的选择
        if HMM:
            cut_blk = self.__cut_DAG
        else:
            cut_blk = self.__cut_DAG_NO_HMM

        # 遍历正则匹配分隔好的语句,对每个语句进行单独的分词和词性标注
        for blk in blocks:
            if re_han_internal.match(blk):
                # 分词和词性标注
                for word in cut_blk(blk):
                    yield word
            else:
                tmp = re_skip_internal.split(blk)
                for x in tmp:
                    if re_skip_internal.match(x):
                        yield pair(x, 'x')
                    else:
                        for xx in x:
                            if re_num.match(xx):
                                yield pair(xx, 'm')
                            elif re_eng.match(x):
                                yield pair(xx, 'eng')
                            else:
                                yield pair(xx, 'x')

4.2 遍历语句,进行分词和词性标注

步骤和jieba分词基本一致,主体步骤如下,详细的每个步骤见

  1. 得到语句的有向无环图DAG
  2. 动态规划构建Route,计算从语句末尾到语句起始,DAG中每个节点到语句结束位置的最大路径概率,以及概率最大时节点对应词语的结束位置
  3. 遍历每个节点的Route,组装词语组合。
  4. 如果词语不在字典中,也就是新词,使用HMM隐马尔科夫模型进行分割
  5. 通过yield将词语逐个返回。
def __cut_DAG(self, sentence):
        # 构建DAG有向无环图,得到语句分词所有可能的路径
        DAG = self.tokenizer.get_DAG(sentence)
        route = {}

        # 动态规划,计算从语句末尾到语句起始,DAG中每个节点到语句结束位置的最大路径概率,以及概率最大时节点对应词语的结束位置
        self.tokenizer.calc(sentence, DAG, route)

        # 遍历每个节点的Route,组装词语组合。
        x = 0
        buf = ''
        N = len(sentence)
        while x &lt; N:
            # y表示词语的结束位置,x为词语的起始位置
            y = route[x][1] + 1
            # 从起始位置x到结束位置y,取出一个词语
            l_word = sentence[x:y]
            if y - x == 1:
                # 单字,一个汉字构成的一个词语
                buf += l_word
            else:
                # 多汉字词语
                if buf:
                    if len(buf) == 1:
                        # 单字直接从字典中取出其词性。使用pair将分词和词性一起输出。
                        yield pair(buf, self.word_tag_tab.get(buf, 'x'))
                    elif not self.tokenizer.FREQ.get(buf):
                        # 词语不在字典中,也就是新词,使用HMM隐马尔科夫模型进行分割
                        recognized = self.__cut_detail(buf)
                        for t in recognized:
                            yield t
                    else:
                        # 词语在字典中,直接查找字典并取出词性。
                        for elem in buf:
                            yield pair(elem, self.word_tag_tab.get(elem, 'x'))
                    buf = ''
                yield pair(l_word, self.word_tag_tab.get(l_word, 'x'))

            # 该节点取词完毕,跳到下一个词语的开始位置
            x = y

        # 通过yield,逐词返回上一步切分好的词语
        if buf:
            if len(buf) == 1:
                yield pair(buf, self.word_tag_tab.get(buf, 'x'))
            elif not self.tokenizer.FREQ.get(buf):
                recognized = self.__cut_detail(buf)
                for t in recognized:
                    yield t
            else:
                for elem in buf:
                    yield pair(elem, self.word_tag_tab.get(elem, 'x'))

其中word_tag_tab在初始化加载词典阶段构建得到,它使用词语为key,对应词性为value。代码如下

def load_word_tag(self, f):
        self.word_tag_tab = {}
        f_name = resolve_filename(f)
        
        # 遍历字典的每一行。每一行对应一个词语。包含词语 词数 词性三部分
        for lineno, line in enumerate(f, 1):
            try:
                # 去除首尾空格符
                line = line.strip().decode("utf-8")
                if not line:
                    continue
                # 利用空格将一行分隔为词语 词数 词性三部分
                word, _, tag = line.split(" ")
                # 使用词语为key,词性为value,构造Dict
                self.word_tag_tab[word] = tag
            except Exception:
                raise ValueError(
                    'invalid POS dictionary entry in %s at Line %s: %s' % (f_name, lineno, line))
        f.close()

4.3 未登录词,HMM隐马尔科夫模型处理

和分词一样,词性标注中,也使用HMM隐马尔科夫模型来处理未登录词。通过大规模语料统计,得到起始概率 发射概率和转移概率。分别对应prob_start.py prob_emit.py和prob_trans.py三个文件,他们给出了词语在BEMS四种情况下,每种词性对应的概率。然后使用viterbi算法,利用得到的三个概率,将观测序列(分词后的语句)转化得到隐藏序列(词性标注序列)。这样就完成了未登录词的词性标注。代码如下。

# 通过HMM隐马尔科夫模型获取词性标注序列,解决未登录的问题
def __cut(self, sentence):
        # 通过viterbi算法,利用三个概率,由语句观测序列,得到词性标注隐藏序列
        # prob为
        # pos_list对应每个汉字,包含分词标注BEMS和词语词性两部分。
        prob, pos_list = viterbi(
            sentence, char_state_tab_P, start_P, trans_P, emit_P)
        begin, nexti = 0, 0

        # 遍历语句的每个汉字,如果是E或者S时,也就是词语结束或者单字词语,则分隔得到词语和词性pair
        for i, char in enumerate(sentence):
            pos = pos_list[i][0]
            if pos == 'B':
                # B表示词语的开始
                begin = i
            elif pos == 'E':
                # E表示词语的结束,此时输出词语和他的词性
                yield pair(sentence[begin:i + 1], pos_list[i][1])
                nexti = i + 1
            elif pos == 'S':
                # S表示单字词语,此时也输出词语和他的词性
                yield pair(char, pos_list[i][1])
                nexti = i + 1

        # 一般不会走到这儿,以防万一。对剩余的所有汉字一起输出一个词语和词性。
        if nexti &lt; len(sentence):
            yield pair(sentence[nexti:], pos_list[nexti][1])

观测序列到隐藏序列的计算,则通过viterbi算法实现。代码如下

# 通过viterbi算法,由观测序列,也就是语句,来得到隐藏序列,也就是BEMS标注序列和词性标注序列
# obs为语句,states为"BEMS"四种状态,
# start_p为起始概率, trans_p为转移概率, emit_p为发射概率,三者通过语料训练得到
def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]  # 每个汉字的每个BEMS状态的最大概率。
    mem_path = [{}]   # 分词路径

    # 初始化每个state,states为"BEMS"
    all_states = trans_p.keys()
    for y in states.get(obs[0], all_states):  # init
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
        mem_path[0][y] = ''

    # 逐字进行处理
    for t in xrange(1, len(obs)):
        V.append({})
        mem_path.append({})
        #prev_states = get_top_states(V[t-1])
        prev_states = [
            x for x in mem_path[t - 1].keys() if len(trans_p[x]) &gt; 0]

        prev_states_expect_next = set(
            (y for x in prev_states for y in trans_p[x].keys()))
        obs_states = set(
            states.get(obs[t], all_states)) &amp; prev_states_expect_next

        if not obs_states:
            obs_states = prev_states_expect_next if prev_states_expect_next else all_states

        # 遍历每个状态
        for y in obs_states:
            # 计算前一个状态到本状态的最大概率和它的前一个状态
            prob, state = max((V[t - 1][y0] + trans_p[y0].get(y, MIN_INF) +
                               emit_p[y].get(obs[t], MIN_FLOAT), y0) for y0 in prev_states)
            # 将该汉字下的某状态(BEMS)的最大概率记下来
            V[t][y] = prob
            # 记录状态转换路径
            mem_path[t][y] = state

    last = [(V[-1][y], y) for y in mem_path[-1].keys()]
    # if len(last)==0:
    #     print obs
    prob, state = max(last)

    route = [None] * len(obs)
    i = len(obs) - 1
    while i &gt;= 0:
        route[i] = state
        state = mem_path[i][state]
        i -= 1
    return (prob, route)

5.总结

jieba可以在分词的同时,完成词性标注,因此标注速度可以得到保证。通过查询字典的方式获取识别词的词性,通过HMM隐马尔科夫模型来获取未登录词的词性,从而完成整个语句的词性标注。但可以看到查询字典的方式不能解决一词多词性的问题,也就是词性歧义问题。故精度上还是有所欠缺的。


NLP三大特征抽取器(CNN RNN TF)

NLP三大特征抽取器(CNN/RNN/TF)

摘自文章:自然语言处理三大特征抽取器

结论:RNN已经基本完成它的历史使命,将来会逐步退出历史舞台;CNN如果改造得当,将来还是有希望有自己在NLP领域的一席之地;而Transformer明显会很快成为NLP里担当大任的最主流的特征抽取器。

NLP任务的特点:输入是个一维线性序列;输入不定长;单词或句子的位置关系很重要;句子中长距离特征对于语义理解也很重要。

一个特征抽取器是否适配问题领域的特点,有时候决定了它的成败,而很多模型改进的方向,其实就是改造得使得它更匹配领域问题的特性。

RNN

采取线性序列结构不断从前往后收集输入信息,但这种线性序列结构在反向传播的时候存在优化困难问题,因为反向传播路径太长,容易导致严重的梯度消失或梯度爆炸问题。为了解决这个问题,后来引入了LSTM和GRU模型,通过增加中间状态信息直接向后传播,以此缓解梯度消失问题,获得了很好的效果,于是很快LSTM和GRU成为RNN的标准模型。经过不断优化,后来NLP又从图像领域借鉴并引入了attention机制(从这两个过程可以看到不同领域的相互技术借鉴与促进作用),叠加网络把层深作深,以及引入Encoder-Decoder框架,这些技术进展极大拓展了RNN的能力以及应用效果。

RNN的结构天然适配解决NLP的问题,NLP的输入往往是个不定长的线性序列句子,而RNN本身结构就是个可以接纳不定长输入的由前向后进行信息线性传导的网络结构,而在LSTM引入三个门后,对于捕获长距离特征也是非常有效的。所以RNN特别适合NLP这种线形序列应用场景,这是RNN为何在NLP界如此流行的根本原因。

RNN在新时代面临的两个问题:

  1. 一些新模型的崛起:特殊改造的CNN;Transformer
  2. RNN结构存在序列依赖,对大规模并行非常不友好

CNN

CNN捕获的特征其实的单词的k-gram片段信息,k的大小决定了能捕获多远距离的特征。

目前NLP界主流的CNN:

通常由1-D卷积层来叠加深度,使用Skip Connection来辅助优化,也可以引入Dilated CNN等手段。

CNN的卷积层其实是保留了相对位置信息的,CNN的并行计算能力,那是非常强的。

Transformer

自然语言一般是个不定长的句子,那么这个不定长问题怎么解决呢?Transformer做法跟CNN是类似的,一般设定输入的最大长度,如果句子没那么长,则用Padding填充,这样整个模型输入起码看起来是定长的了。

三大抽取器比较

  1. 语义特征提取能力:Transformer在这方面的能力非常显著地超过RNN和CNN,RNN和CNN两者能力差不太多。
  2. 长距离特征捕获能力:原生CNN特征抽取器在这方面极为显著地弱于RNN和Transformer,Transformer微弱优于RNN模型(尤其在主语谓语距离小于13时),能力由强到弱排序为Transformer>RNN>>CNN; 但在比较远的距离上(主语谓语距离大于13),RNN微弱优于Transformer,所以综合看,可以认为Transformer和RNN在这方面能力差不太多,而CNN则显著弱于前两者。
  3. 任务综合特征抽取能力(机器翻译):Transformer综合能力要明显强于RNN和CNN,而RNN和CNN看上去表现基本相当,貌似CNN表现略好一些。
  4. 并行计算能力及运行效率:RNN在并行计算方面有严重缺陷,这是它本身的序列依赖特性导致的;对于CNN和Transformer来说,因为它们不存在网络中间状态不同时间步输入的依赖关系,所以可以非常方便及自由地做并行计算改造。Transformer和CNN差不多,都远远远远强于RNN。

综合排名

单从任务综合效果方面来说,Transformer明显优于CNN,CNN略微优于RNN。速度方面Transformer和CNN明显占优,RNN在这方面劣势非常明显。

三者的结合:向Transformer靠拢


llm概念

1.llm概念

\[toc]

1.目前 主流的开源模型体系 有哪些?

目前主流的开源LLM(语言模型)模型体系包括以下几个:

  1. GPT(Generative Pre-trained Transformer)系列:由OpenAI发布的一系列基于Transformer架构的语言模型,包括GPT、GPT-2、GPT-3等。GPT模型通过在大规模无标签文本上进行预训练,然后在特定任务上进行微调,具有很强的生成能力和语言理解能力。
  2. BERT(Bidirectional Encoder Representations from Transformers):由Google发布的一种基于Transformer架构的双向预训练语言模型。BERT模型通过在大规模无标签文本上进行预训练,然后在下游任务上进行微调,具有强大的语言理解能力和表征能力。
  3. XLNet:由CMU和Google Brain发布的一种基于Transformer架构的自回归预训练语言模型。XLNet模型通过自回归方式预训练,可以建模全局依赖关系,具有更好的语言建模能力和生成能力。
  4. RoBERTa:由Facebook发布的一种基于Transformer架构的预训练语言模型。RoBERTa模型在BERT的基础上进行了改进,通过更大规模的数据和更长的训练时间,取得了更好的性能。
  5. T5(Text-to-Text Transfer Transformer):由Google发布的一种基于Transformer架构的多任务预训练语言模型。T5模型通过在大规模数据集上进行预训练,可以用于多种自然语言处理任务,如文本分类、机器翻译、问答等。

这些模型在自然语言处理领域取得了显著的成果,并被广泛应用于各种任务和应用中。

2.prefix LM 和 causal LM 区别是什么?

Prefix LM(前缀语言模型)和Causal LM(因果语言模型)是两种不同类型的语言模型,它们的区别在于生成文本的方式和训练目标。

2.1 Prefix LM

Prefix LM其实是Encoder-Decoder模型的变体,为什么这样说?解释如下:

  1. 在标准的Encoder-Decoder模型中,Encoder和Decoder各自使用一个独立的Transformer
  2. 而在Prefix LM,Encoder和Decoder则共享了同一个Transformer结构,在Transformer内部通过Attention Mask机制来实现。

与标准Encoder-Decoder类似,Prefix LM在Encoder部分采用Auto Encoding (AE-自编码)模式,即前缀序列中任意两个token都相互可见,而Decoder部分采用Auto Regressive (AR-自回归)模式,即待生成的token可以看到Encoder侧所有token(包括上下文)和Decoder侧已经生成的token,但不能看未来尚未产生的token。

下面的图很形象地解释了Prefix LM的Attention Mask机制(左)及流转过程(右)。

Prefix LM的代表模型有UniLM、GLM

2.2 Causal LM

Causal LM是因果语言模型,目前流行地大多数模型都是这种结构,别无他因,因为GPT系列模型内部结构就是它,还有开源界的LLaMa也是。

Causal LM只涉及到Encoder-Decoder中的Decoder部分,采用Auto Regressive模式,直白地说,就是根据历史的token来预测下一个token,也是在Attention Mask这里做的手脚。

参照着Prefix LM,可以看下Causal LM的Attention Mask机制(左)及流转过程(右)。

2.3 总结

  1. Prefix LM:前缀语言模型是一种生成模型,它在生成每个词时都可以考虑之前的上下文信息。在生成时,前缀语言模型会根据给定的前缀(即部分文本序列)预测下一个可能的词。这种模型可以用于文本生成、机器翻译等任务。
  2. Causal LM:因果语言模型是一种自回归模型,它只能根据之前的文本生成后续的文本,而不能根据后续的文本生成之前的文本。在训练时,因果语言模型的目标是预测下一个词的概率,给定之前的所有词作为上下文。这种模型可以用于文本生成、语言建模等任务。

总结来说,前缀语言模型可以根据给定的前缀生成后续的文本,而因果语言模型只能根据之前的文本生成后续的文本。它们的训练目标和生成方式略有不同,适用于不同的任务和应用场景。

3.大模型LLM的 训练目标

大型语言模型(Large Language Models,LLM)的训练目标通常是最大似然估计(Maximum Likelihood Estimation,MLE)。最大似然估计是一种统计方法,用于从给定数据中估计概率模型的参数。

在LLM的训练过程中,使用的数据通常是大量的文本语料库。训练目标是最大化模型生成训练数据中观察到的文本序列的概率。具体来说,对于每个文本序列,模型根据前面的上下文生成下一个词的条件概率分布,并通过最大化生成的词序列的概率来优化模型参数。

为了最大化似然函数,可以使用梯度下降等优化算法来更新模型参数,使得模型生成的文本序列的概率逐步提高。在训练过程中,通常会使用批量训练(batch training)的方法,通过每次处理一小批数据样本来进行参数更新。

4.涌现能力是啥原因?

大语言模型的涌现能力:现象与解释 - 知乎 (zhihu.com)

涌现能力(Emergent Ability)是指模型在训练过程中能够生成出令人惊喜、创造性和新颖的内容或行为。这种能力使得模型能够超出其训练数据所提供的内容,并产生出具有创造性和独特性的输出。

涌现能力的产生可以归因于以下几个原因:

  1. 任务的评价指标不够平滑:因为很多任务的评价指标不够平滑,导致我们现在看到的涌现现象。如果评价指标要求很严格,要求一字不错才算对,那么Emoji\_movie任务我们就会看到涌现现象的出现。但是,如果我们把问题形式换成多选题,就是给出几个候选答案,让LLM选,那么随着模型不断增大,任务效果在持续稳定变好,但涌现现象消失,如上图图右所示。这说明评价指标不够平滑,起码是一部分任务看到涌现现象的原因。
  2. 复杂任务 vs 子任务:展现出涌现现象的任务有一个共性,就是任务往往是由多个子任务构成的复杂任务。也就是说,最终任务过于复杂,如果仔细分析,可以看出它由多个子任务构成,这时候,子任务效果往往随着模型增大,符合 Scaling Law,而最终任务则体现为涌现现象。
  3. 用 Grokking (顿悟)来解释涌现:对于某个任务T,尽管我们看到的预训练数据总量是巨大的,但是与T相关的训练数据其实数量很少。当我们推大模型规模的时候,往往会伴随着增加预训练数据的数据量操作,这样,当模型规模达到某个点的时候,与任务T相关的数据量,突然就达到了最小要求临界点,于是我们就看到了这个任务产生了Grokking现象。

尽管涌现能力为模型带来了创造性和独特性,但也需要注意其生成的内容可能存在偏差、错误或不完整性。因此,在应用和使用涌现能力强的模型时,需要谨慎评估和验证生成的输出,以确保其质量和准确性。

5.为何现在的大模型大部分是Decoder only结构

  1. Encoder的低秩问题:Encoder的双向注意力会存在低秩问题,这可能会削弱模型表达能力,就生成任务而言,引入双向注意力并无实质好处。
  2. 更好的Zero-Shot性能、更适合于大语料自监督学习:decoder-only 模型在没有任何 tuning 数据的情况下、zero-shot 表现最好,而 encoder-decoder 则需要在一定量的标注数据上做 multitask finetuning 才能激发最佳性能。
  3. 效率问题:decoder-only支持一直复用KV-Cache,对多轮对话更友好,因为每个Token的表示之和它之前的输入有关,而encoder-decoder和PrefixLM就难以做到。

6.大模型架构介绍

Transformer 模型一开始是用来做 seq2seq 任务的,所以它包含 Encoder 和 Decoder 两个部分;他们两者的区别主要是,Encoder 在抽取序列中某一个词的特征时能够看到整个序列中所有的信息,即上文和下文同时看到;而 Decoder 中因为有 mask 机制的存在,使得它在编码某一个词的特征时只能看到自身和它之前的文本信息。

首先概述几种主要的架构:

  • 以BERT为代表的encoder-only
  • 以T5和BART为代表的encoder-decoder
  • 以GPT为代表的decoder-only,
  • 以UNILM9为代表的PrefixLM(相比于GPT只改了attention mask,前缀部分是双向,后面要生成的部分是单向的causal mask%)

6.LLMs复读机问题

6.1 什么是 LLMs 复读机问题?

LLMs复读机问题(LLMs Parroting Problem)是指大型语言模型在生成文本时过度依赖输入文本的复制,而缺乏创造性和独特性。当面对一个问题或指令时,模型可能会简单地复制输入文本的一部分或全部内容,并将其作为生成的输出,而不是提供有意义或新颖的回应。

6.2 为什么会出现 LLMs 复读机问题?

  1. 数据偏差:大型语言模型通常是通过预训练阶段使用大规模无标签数据进行训练的。如果训练数据中存在大量的重复文本或者某些特定的句子或短语出现频率较高,模型在生成文本时可能会倾向于复制这些常见的模式。
  2. 训练目标的限制:大型语言模型的训练通常是基于自监督学习的方法,通过预测下一个词或掩盖词来学习语言模型。这样的训练目标可能使得模型更倾向于生成与输入相似的文本,导致复读机问题的出现。
  3. 缺乏多样性的训练数据:虽然大型语言模型可以处理大规模的数据,但如果训练数据中缺乏多样性的语言表达和语境,模型可能无法学习到足够的多样性和创造性,导致复读机问题的出现。
  4. 模型结构和参数设置:大型语言模型的结构和参数设置也可能对复读机问题产生影响。例如,模型的注意力机制和生成策略可能导致模型更倾向于复制输入的文本。

6.3 如何缓解 LLMs 复读机问题?

为了缓解LLMs复读机问题,可以尝试以下方法:

  1. 多样性训练数据:在训练阶段,使用多样性的语料库来训练模型,避免数据偏差和重复文本的问题。这可以包括从不同领域、不同来源和不同风格的文本中获取数据。
  2. 引入噪声:在生成文本时,引入一些随机性或噪声,例如通过采样不同的词或短语,或者引入随机的变换操作,以增加生成文本的多样性。这可以通过在生成过程中对模型的输出进行采样或添加随机性来实现。
  3. 温度参数调整:温度参数是用来控制生成文本的多样性的一个参数。通过调整温度参数的值,可以控制生成文本的独创性和多样性。较高的温度值会增加随机性,从而减少复读机问题的出现。
  4. Beam搜索调整:在生成文本时,可以调整Beam搜索算法的参数。Beam搜索是一种常用的生成策略,它在生成过程中维护了一个候选序列的集合。通过调整Beam大小和搜索宽度,可以控制生成文本的多样性和创造性。
  5. 后处理和过滤:对生成的文本进行后处理和过滤,去除重复的句子或短语,以提高生成文本的质量和多样性。可以使用文本相似度计算方法或规则来检测和去除重复的文本。
  6. 人工干预和控制:对于关键任务或敏感场景,可以引入人工干预和控制机制,对生成的文本进行审查和筛选,确保生成结果的准确性和多样性。

需要注意的是,缓解LLMs复读机问题是一个复杂的任务,没有一种通用的解决方案。不同的方法可能适用于不同的场景和任务,需要根据具体情况进行选择和调整。此外,解决复读机问题还需要综合考虑数据、训练目标、模型架构和生成策略等多个因素,需要进一步的研究和实践来提高大型语言模型的生成文本多样性和创造性。

7.LLMs输入句子长度理论上可以无限长吗?

理论上来说,LLMs(大型语言模型)可以处理任意长度的输入句子,但实际上存在一些限制和挑战。下面是一些相关的考虑因素:

  1. 计算资源:生成长句子需要更多的计算资源,包括内存和计算时间。由于LLMs通常是基于神经网络的模型,计算长句子可能会导致内存不足或计算时间过长的问题。
  2. 模型训练和推理:训练和推理长句子可能会面临一些挑战。在训练阶段,处理长句子可能会导致梯度消失或梯度爆炸的问题,影响模型的收敛性和训练效果。在推理阶段,生成长句子可能会增加模型的错误率和生成时间。
  3. 上下文建模:LLMs是基于上下文建模的模型,长句子的上下文可能会更加复杂和深层。模型需要能够捕捉长句子中的语义和语法结构,以生成准确和连贯的文本。

8.什么情况用Bert模型,什么情况用LLaMA、ChatGLM类大模型,咋选?

选择使用哪种大模型,如Bert、LLaMA或ChatGLM,取决于具体的应用场景和需求。下面是一些指导原则:

  1. Bert模型:Bert是一种预训练的语言模型,适用于各种自然语言处理任务,如文本分类、命名实体识别、语义相似度计算等。如果你的任务是通用的文本处理任务,而不依赖于特定领域的知识或语言风格,Bert模型通常是一个不错的选择。Bert由一个Transformer编码器组成,更适合于NLU相关的任务。
  2. LLaMA模型:LLaMA(Large Language Model Meta AI)包含从 7B 到 65B 的参数范围,训练使用多达14,000亿tokens语料,具有常识推理、问答、数学推理、代码生成、语言理解等能力。LLaMA由一个Transformer解码器组成。训练预料主要为以英语为主的拉丁语系,不包含中日韩文。所以适合于英文文本生成的任务。
  3. ChatGLM模型:ChatGLM是一个面向对话生成的语言模型,适用于构建聊天机器人、智能客服等对话系统。如果你的应用场景需要模型能够生成连贯、流畅的对话回复,并且需要处理对话上下文、生成多轮对话等,ChatGLM模型可能是一个较好的选择。ChatGLM的架构为Prefix decoder,训练语料为中英双语,中英文比例为1:1。所以适合于中文和英文文本生成的任务。

在选择模型时,还需要考虑以下因素:

  • 数据可用性:不同模型可能需要不同类型和规模的数据进行训练。确保你有足够的数据来训练和微调所选择的模型。
  • 计算资源:大模型通常需要更多的计算资源和存储空间。确保你有足够的硬件资源来支持所选择的模型的训练和推理。
  • 预训练和微调:大模型通常需要进行预训练和微调才能适应特定任务和领域。了解所选择模型的预训练和微调过程,并确保你有相应的数据和时间来完成这些步骤。

最佳选择取决于具体的应用需求和限制条件。在做出决策之前,建议先进行一些实验和评估,以确定哪种模型最适合你的应用场景。

9.各个专业领域是否需要各自的大模型来服务?

各个专业领域通常需要各自的大模型来服务,原因如下:

  1. 领域特定知识:不同领域拥有各自特定的知识和术语,需要针对该领域进行训练的大模型才能更好地理解和处理相关文本。例如,在医学领域,需要训练具有医学知识的大模型,以更准确地理解和生成医学文本。
  2. 语言风格和惯用语:各个领域通常有自己独特的语言风格和惯用语,这些特点对于模型的训练和生成都很重要。专门针对某个领域进行训练的大模型可以更好地掌握该领域的语言特点,生成更符合该领域要求的文本。
  3. 领域需求的差异:不同领域对于文本处理的需求也有所差异。例如,金融领域可能更关注数字和统计数据的处理,而法律领域可能更关注法律条款和案例的解析。因此,为了更好地满足不同领域的需求,需要专门针对各个领域进行训练的大模型。
  4. 数据稀缺性:某些领域的数据可能相对较少,无法充分训练通用的大模型。针对特定领域进行训练的大模型可以更好地利用该领域的数据,提高模型的性能和效果。

尽管需要各自的大模型来服务不同领域,但也可以共享一些通用的模型和技术。例如,通用的大模型可以用于处理通用的文本任务,而领域特定的模型可以在通用模型的基础上进行微调和定制,以适应特定领域的需求。这样可以在满足领域需求的同时,减少模型的重复训练和资源消耗。

10.如何让大模型处理更长的文本?

要让大模型处理更长的文本,可以考虑以下几个方法:

  1. 分块处理:将长文本分割成较短的片段,然后逐个片段输入模型进行处理。这样可以避免长文本对模型内存和计算资源的压力。在处理分块文本时,可以使用重叠的方式,即将相邻片段的一部分重叠,以保持上下文的连贯性。
  2. 层次建模:通过引入层次结构,将长文本划分为更小的单元。例如,可以将文本分为段落、句子或子句等层次,然后逐层输入模型进行处理。这样可以减少每个单元的长度,提高模型处理长文本的能力。
  3. 部分生成:如果只需要模型生成文本的一部分,而不是整个文本,可以只输入部分文本作为上下文,然后让模型生成所需的部分。例如,输入前一部分文本,让模型生成后续的内容。
  4. 注意力机制:注意力机制可以帮助模型关注输入中的重要部分,可以用于处理长文本时的上下文建模。通过引入注意力机制,模型可以更好地捕捉长文本中的关键信息。
  5. 模型结构优化:通过优化模型结构和参数设置,可以提高模型处理长文本的能力。例如,可以增加模型的层数或参数量,以增加模型的表达能力。还可以使用更高效的模型架构,如Transformer等,以提高长文本的处理效率。

需要注意的是,处理长文本时还需考虑计算资源和时间的限制。较长的文本可能需要更多的内存和计算时间,因此在实际应用中需要根据具体情况进行权衡和调整。

本文由 GitVP 从 GitHub 收录并在站内全文呈现,版权归原作者所有。

← 回到全部文章

同分类还有