【大模型实战篇】大模型分词算法Unigram及代码示例
1. 算法原理介绍
与 BPE 分词(参考《BPE原理及代码示例》)和 WordPiece 分词(参考《WordPiece原理及代码示例》)不同,Unigram 分词方法【1】是从一个包含足够多字符串或词元的初始集合开始,迭代地删除其中的词元,直到达到预期的词表大小。该方法假设通过删除某个词元能够增加训练语料的似然性,并以此作为选择标准。这个过程依赖于一个训练好的单一语言模型。
为了估计单一语言模型,在每次迭代中,首先根据旧的语言模型找到当前最优的分词方式,然后重新估计单一概率以更新语言模型。通常,使用动态规划算法(如维特比算法,Viterbi Algorithm)来高效找到语言模型对词汇的最优分词方案。
Unigram算法通常用于SentencePiece,这是AlBERT、T5、mBART、Big Bird和XLNet等模型使用的分词算法。如前述,Unigram训练算法与BPE和WordPiece相比,Unigram的工作方式相反:它从一个大词汇开始,逐步删除其中的标记,直到达到所需的词汇大小。可以使用多种方法构建基础词汇,例如提取预分词单词中最常见的子串,或在具有较大词汇量的初始语料库上应用BPE。
在训练的每一步,Unigram算法根据当前词汇计算语料库的损失。然后,对于词汇中的每个符号,算法计算如果移除该符号整体损失将增加多少,并寻找对整体损失影响最小的符号。这些符号对语料库的整体损失影响较小,因此在某种意义上,它们是“需求较低”的,最适合被删除。可以预见,这个过程非常耗时。需要注意基本字符绝不删除,以确保任何单词都可以被分词。
2. 案例说明
重用在BPE中的语料库:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
在这个例子中,将所有严格子串作为初始词汇:
["h", "u", "g", "hu", "ug", "p", "pu", "n", "un", "b", "bu", "s", "hug", "gs", "ugs"]
分词算法 Unigram模型是一种语言模型,认为每个标记与之前的标记是独立的。这是最简单的语言模型,因为给定之前的上下文,标记X的概率仅为标记X的概率。因此,如果使用Unigram语言模型生成文本,总是会预测最常见的标记。给定标记的概率是其在原始语料库中的频率(出现次数),除以词汇中所有标记频率的总和(以确保概率总和为1)。例如,“ug”出现在“hug”、“pug”和“hugs”中,因此在当前语料库中它的频率为20。
以下是词汇中所有可能子词的频率:
("h", 15) ("u", 36) ("g", 20) ("hu", 15) ("ug", 20) ("p", 17) ("pu", 17) ("n", 16) ("un", 16) ("b", 4) ("bu", 4) ("s", 5) ("hug", 15) ("gs", 5) ("ugs", 5)
因此,所有频率的总和为210,而子词“ug”的概率为20/210。
现在,为了对给定单词进行分词,查看所有可能的分词方式,并根据Unigram模型计算每种方式的概率。由于所有标记被视为独立,因此该概率只是每个标记概率的乘积。例如,单词“pug”的分词["p", "u", "g"]的概率为:
P ( [ "p" , "u" , "g" ] ) = P ( "p" ) × P ( "u" ) × P ( "g" ) = 5/210 × 36/210 × 20/210= 0.000389
相比之下,分词["pu", "g"]的概率为:
P ( [ "pu" , "g" ] ) = P ( "pu" ) × P ( "g" ) = 5/210 × 20/210 = 0.0022676
因此,这种分词的概率要高得多。一般来说,标记数量最少的分词将具有最高的概率(因为对于每个标记都有210的除法),这对应于直观上的目的:将单词分成尽可能少的标记。
使用Unigram模型对单词进行分词的结果是概率最高的分词。在“pug”的例子中,为每个可能的分词计算的概率如下:
["p", "u", "g"] : 0.000389
["p", "ug"] : 0.0022676
["pu", "g"] : 0.0022676
因此,“pug”的分词将为["p", "ug"]或["pu", "g"],具体取决于首先遇到的分词。
在这个情况下,很容易找到所有可能的分词并计算它们的概率,但通常这会更困难。采用维特比算法,可以构建一个图来检测给定单词的可能分词,通过说如果从字符a到字符b有一个子词,则从a到b有一个分支,并将子词的概率赋予该分支。
为了找到在该图中具有最佳分数的路径,维特比算法确定每个单词位置的最佳分词,它结束于该位置。由于是从开始到结束,因此通过遍历所有以当前字符结束的子词来找到最佳分数,然后使用该子词起始位置的最佳分词分数。接下来,只需解开到达结尾的路径。
看一个使用我们的词汇和单词“unhug”的例子。对于每个位置,最佳分数的子词如下:
字符0 (u): "u" (分数0.171429)
字符1 (n): "un" (分数0.076191)
字符2 (h): "un" "h" (分数0.005442)
字符3 (u): "un" "hu" (分数0.005442)
字符4 (g): "un" "hug" (分数0.005442)
因此,“unhug”的分词将为["un", "hug"]。
基于上述例子,已经了解了分词是如何工作的,进一步探讨训练期间使用的损失函数。在任何给定阶段,这个损失是通过对语料库中的每个单词进行分词计算的,使用当前的词汇表和根据每个标记在语料库中的频率确定的单语模型。
语料库中的每个单词都有一个分数,损失则是这些分数的负对数似然——即语料库中所有单词的 -log(P(单词)) 的总和。
回到以下示例语料库:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
每个单词的分词及其相应的分数为:
"hug": ["hug"] (分数 0.071428)
"pug": ["pu", "g"] (分数 0.007710)
"pun": ["pu", "n"] (分数 0.006168)
"bun": ["bu", "n"] (分数 0.001451)
"hugs": ["hug", "s"] (分数 0.001701)
因此,损失为:
10 * (-log(0.071428)) + 5 * (-log(0.007710)) + 12 * (-log(0.006168)) + 4 * (-log(0.001451)) + 5 * (-log(0.001701)) = 169.8
现在需要计算每个标记的移除如何影响损失。为了简化,只为两个标记进行计算,并将整个过程留到有代码帮助时再进行。在这个特殊的情况下,有两个等价的标记化:例如,"pug" 可以标记为 ["p", "ug"],并具有相同的分数。因此,从词汇表中移除 "pu" 将产生相同的损失。
另一方面,移除 "hug" 将导致损失增加,因为 "hug" 和 "hugs" 的标记化将变为:
"hug": ["hu", "g"] (分数 0.006802)
"hugs": ["hu", "gs"] (分数 0.001701)
这些变化将导致损失上升:
10 * (-log(0.071428)) + 10 * (-log(0.006802)) = 23.5
因此,标记 "pu" 可能会从词汇表中移除,但 "hug" 则不会。
3. 代码实现
依然沿用在BPE中的语料,在jupyter环境进行测试。本来我们验证的预训练模型为xlnet-base-cased,但model scope没有对应的,所以我们更换成T5,效果是一样的,因为使用的分词器是一样的。
corpus = ["This is the Hugging Face Course.","This chapter is about tokenization.","This section shows several tokenizer algorithms.","Hopefully, you will be able to understand how they are trained and generate tokens.",
]
同样,下载和加载T5预训练模型:
import torch
from modelscope import snapshot_download, AutoModel, AutoTokenizer
import os
from transformers import AutoTokenizermodel_dir = snapshot_download('AI-ModelScope/t5-base', cache_dir='/root/autodl-tmp', revision='master')
mode_name_or_path = '/root/autodl-tmp/AI-ModelScope/t5-base'
tokenizer = AutoTokenizer.from_pretrained(mode_name_or_path, trust_remote_code=True)
统计预分词的词频:
from collections import defaultdictword_freqs = defaultdict(int)
for text in corpus:words_with_offsets = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)new_words = [word for word, offset in words_with_offsets]for word in new_words:word_freqs[word] += 1word_freqs
接下来,需要将词汇初始化为一个比我们最终希望的词汇量更大的值。必须包含所有基本字符(否则将无法标记每个单词),但对于较大的子字符串,将仅保留最常见的,因此按频率对它们进行排序:
char_freqs = defaultdict(int)
subwords_freqs = defaultdict(int)
for word, freq in word_freqs.items():for i in range(len(word)):char_freqs[word[i]] += freq# 循环遍历至少长度为2的子单词for j in range(i + 2, len(word) + 1):subwords_freqs[word[i:j]] += freq# 按频率对子单词进行排序
sorted_subwords = sorted(subwords_freqs.items(), key=lambda x: x[1], reverse=True)
sorted_subwords[:10]
将字符与最佳子单词分组,以达到初始词汇大小 300:
token_freqs = list(char_freqs.items()) + sorted_subwords[: 300 - len(char_freqs)]
token_freqs = {token: freq for token, freq in token_freqs}
接下来,计算所有频率的总和,将频率转换为概率。对于我们的模型,将存储概率的对数,因为将对数相加比乘以小数字更数值稳定,并且这将简化模型损失的计算:
from math import logtotal_sum = sum([freq for token, freq in token_freqs.items()])
model = {token: -log(freq / total_sum) for token, freq in token_freqs.items()}
现在,主要函数是使用维特比算法进行单词标记化。如前所述,该算法计算每个子字符串的最佳分段,将其存储在名为 best_segmentations 的变量中。为单词中的每个位置(从 0 到单词的总长度)存储一个字典,包含两个键:最佳分段中最后一个标记的起始索引,以及最佳分段的分数。通过最后一个标记的起始索引,将能够在列表完全填充后检索完整的分段。
填充列表只需两个循环:主循环遍历每个起始位置,第二个循环尝试从该起始位置开始的所有子字符串。如果子字符串在词汇中,我们就得到了到该结束位置的单词的新分段,将其与 best_segmentations 中的内容进行比较。一旦主循环完成,只需从末尾开始,跳过一个起始位置,记录标记,直到到达单词的起始位置:
def encode_word(word, model):best_segmentations = [{"start": 0, "score": 1}] + [{"start": None, "score": None} for _ in range(len(word))]for start_idx in range(len(word)):# This should be properly filled by the previous steps of the loopbest_score_at_start = best_segmentations[start_idx]["score"]for end_idx in range(start_idx + 1, len(word) + 1):token = word[start_idx:end_idx]if token in model and best_score_at_start is not None:score = model[token] + best_score_at_start# If we have found a better segmentation ending at end_idx, we updateif (best_segmentations[end_idx]["score"] is Noneor best_segmentations[end_idx]["score"] > score):best_segmentations[end_idx] = {"start": start_idx, "score": score}segmentation = best_segmentations[-1]if segmentation["score"] is None:# We did not find a tokenization of the word -> unknownreturn ["<unk>"], Nonescore = segmentation["score"]start = segmentation["start"]end = len(word)tokens = []while start != 0:tokens.insert(0, word[start:end])next_start = best_segmentations[start]["start"]end = startstart = next_starttokens.insert(0, word[start:end])return tokens, score
在一些单词上尝试下初始模型:
计算模型在语料库上的损失:
def compute_loss(model):loss = 0for word, freq in word_freqs.items():_, word_loss = encode_word(word, model)loss += freq * word_lossreturn loss
计算删除每个标记后模型的损失:
import copydef compute_scores(model):scores = {}model_loss = compute_loss(model)for token, score in model.items():# 始终保留长度为1的标记if len(token) == 1:continuemodel_without_token = copy.deepcopy(model)_ = model_without_token.pop(token)scores[token] = compute_loss(model_without_token) - model_lossreturn scores
由于 "ll" 用于 "Hopefully" 的标记化,移除它可能会导致使用标记 "l" 两次,因此预期它会产生正损失。
这种方法效率非常低,因此 SentencePiece 使用了一个近似的模型损失计算:它并不是从头开始,而是用剩余词汇中标记 X 的分割替换标记 X。这样,所有分数可以在计算模型损失的同时一次性计算出来。在此基础上,需要做的最后一件事是将模型使用的特殊标记添加到词汇表中,然后循环直到我们从词汇表中修剪出足够的标记以达到所需大小:
percent_to_remove = 0.1
while len(model) > 100:scores = compute_scores(model)sorted_scores = sorted(scores.items(), key=lambda x: x[1])# 移除具有最低分数的 percent_to_remove 标记。for i in range(int(len(model) * percent_to_remove)):_ = token_freqs.pop(sorted_scores[i][0])total_sum = sum([freq for token, freq in token_freqs.items()])model = {token: -log(freq / total_sum) for token, freq in token_freqs.items()}
然后,为了对一些文本进行标记化,只需应用预标记化,然后使用 encode_word()
函数:
def tokenize(text, model):words_with_offsets = tokenizer.backend_tokenizer.pre_tokenizer.pre_tokenize_str(text)pre_tokenized_text = [word for word, offset in words_with_offsets]encoded_words = [encode_word(word, model)[0] for word in pre_tokenized_text]return sum(encoded_words, [])tokenize("This is the amazing course.", model)
4. 参考材料
【1】Unigram tokenization
相关文章:

【大模型实战篇】大模型分词算法Unigram及代码示例
1. 算法原理介绍 与 BPE 分词(参考《BPE原理及代码示例》)和 WordPiece 分词(参考《WordPiece原理及代码示例》)不同,Unigram 分词方法【1】是从一个包含足够多字符串或词元的初始集合开始,迭代地删除其中的…...
Dockerfile搭建ELK
使用 Dockerfile 安装 ELK 一、引言 ELK Stack(Elasticsearch, Logstash, Kibana)是一种流行的日志管理和分析解决方案。它允许用户实时搜索、分析和可视化日志数据。通过 Docker,可以方便地部署 ELK ,快速获取一个功能齐全的日…...
在合规的地方怎么用EACO地球链兑换交换价值?
地球链EACO(EarthChain,简称$E)是一种虚拟数字资产。 目前在中国大陆,虚拟资产相关业务活动属于金融活动,包括虚拟资产的交易、兑换等操作,因此应该谨慎去寻求如何用它来交换价值。 虚拟资产交易炒作活动&…...

VS无法安装Win10SDK_10.0.2200,快捷方法
Visual Studio无法安装Win10SDK_10.0.2200,我在安装VS2019、2022提示,软件就不能编译。 因为之前安装过VS软件,重新安装软件提示“无法安装”。 原因 之前安装在D盘,现在没有D盘了 说明 因为电脑第一次安装VS,会自动安…...
github多个账号配置多个SSH秘钥
背景 对于有多个github账号的同学,需要配置多个ssh秘钥分别管理多个账号。 方法 1、生成多个SSH秘钥 # 为第一个 GitHub 账号生成密钥 ssh-keygen -t ed25519 -C "your_email_1example.com" -f ~/.ssh/id_ed25519_github_work# 为第二个 GitHub 账号生…...
静态/动态代理详解,一次性看完再也不会搞不清!
代理官方原文翻译: 给其他对象提供一个代理或者占位符,来控制对这个对象的访问。 代理最核心的思想: 在客户端和目标对象之间创建一个“中介”,用于保护目标对象和增强目标对象 静态代理: 该代理对象需要我们手动…...

Webserver(2)GCC
目录 安装GCCVScode远程连接到虚拟机编写代码gcc编译过程gcc与g的区别Xftp连接虚拟机上传文件 安装GCC sudo apt install gcc g查看版本是7.5 touch test.c创建代码 但是在虚拟机中写代码很不方便 VScode远程连接到虚拟机编写代码 gcc test.c -o app在虚拟机中用gcc编译的…...

mac电脑设置chrome浏览器语言切换为日语英语等不生效问题
在chrome中设置了语言,并且已经置顶了,但是不生效,在windows上直接有设置当前语言为chrome显示语言,但是mac上没有。 解决办法 在系统里面有一个单独给chrome设置语言的: 单独给它设定成指定的语言,然后重…...

Python中的人工智能框架与实例
在人工智能(AI)领域,Python因其简洁的语法、丰富的库和强大的社区支持,成为了最受欢迎的编程语言之一。本文将详细介绍Python中的人工智能框架,并通过具体实例展示如何使用这些框架来实现不同的人工智能应用。 一、Python中的人工智能框架 …...

论文阅读(二十六):Dual Attention Network for Scene Segmentation
文章目录 1.Introduction3.DANet3.1Position Attention Module3.2Channel Attention Module 论文:Dual Attention Network for Scene Segmentation 论文链接:Dual Attention Network for Scene Segmentation 代码链接:Github 1.Intr…...

Stack和Queue(3)
Stack和Queue(3) priority_queue的模拟实现 priority_queue.h #include <vector>namespace soobin {template<class T, class Container vector<T>>class priority_queue{public://强制生成默认构造priority_queue() default;temp…...

怎样把学生的成绩单独告知家长?
期中考试季的到来让校园里的气氛似乎也变得紧张起来。家长们开始频繁地联系老师,希望了解孩子的表现;孩子们则在考试后,绞尽脑汁地想出各种理由,以期在成绩不理想时能减轻家长的失望。老师们更是忙得不可开交,不仅要批…...
vue3父组件控制子组件表单验证及获取子组件数值方法
1、关键部分的代码如下,我努力交代清楚了,希望能让大家看懂。 <template><KeepAlive><component ref"comp" :is"compNames[steps[compIndex].comp]" /></KeepAlive><el-button click"prevBtn"…...

【JavaEE】【多线程】单例模式
目录 一、设计模式1.1 单例模式1.1.1 饿汉模式1.1.2 懒汉模式 1.2 线程安全问题1.3 懒汉模式线程安全问题的解决方法1.3.1 原子性问题解决1.3.2 解决效率问题1.3.3 解决内存可见性问题和指令重排序问题 一、设计模式 在讲解案例前,先介绍一个概念设计模式ÿ…...

Java.6--多态-设计模式-抽象父类-抽象方法
一、多态 1.定义--什么是多态? a.同一个父类的不同子类对象,在做同一行为的时候,有不同的表现形式,这就是多态。(总结为:一个父类下的不同子类,同一行为,不同表现形式。࿰…...

JAVA Maven 的安装与配置
一、下载地址 官方网站:Maven – Download Apache Maven 我这里是3.8.6版本 二、安装步骤 maven安装之前要先安装jdk,请确保你的系统已经安装了jdk环境。 1.将下载好的 Maven 进行解压 apache-maven-3.6.8-bin.zip 2.配置本地仓库:修改 conf/settin…...

【程序分享】PCB元件坐标对齐工具 V1.3
↑↑↑点击上方蓝字,关注我们! “PCB元件坐标对齐工具 V1.3”脚本程序在PCB文档中将元件的坐标自动移动到参考圆弧的中心,参考圆弧支持机械层1层和禁止布线层,参考图元的位置任意,不局限于栅格位置。 程序会自动…...

[bug] vllm 0.6.1 RuntimeError: operator torchvision::nms does not exist
[bug] vllm 0.6.1 RuntimeError: operator torchvision::nms does not exist 环境 python 3.10 torch 2.4.0cu118 torchvision 0.19.0cu118 vllm 0.6.1.post2cu118问题详情 if torch._C._d…...

处理Hutool的Http工具上传大文件报OOM
程序环境 JDK版本: 1.8Hutool版本: 5.8.25 问题描述 客服端文件上传主要代码: HttpRequest httpRequest HttpUtil.createPost(FILE_UPLOAD_URL); Resource urlResource new UrlResource(url, fileName); httpRequest.form("file&q…...

transforms的使用
示例代码 from PIL import Image from torch.utils.tensorboard import SummaryWriter from torchvision import transforms#打开该图片 img_path"hymenoptera_data/val/bees/10870992_eebeeb3a12.jpg" imgImage.open(img_path) writerSummaryWriter("logs&quo…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...