有时候我也想听听,我在你心里,是什么样子
—— 25.1.12
一、什么是语言模型
语言是灵活的,也是有规律的
了解一门语言的人可以判断一句话是否“合理”
通俗来讲,语言模型用来评价一句话(句子可以看作是字的组合)是否“合理”或“是人话”
数学上讲,P(今天天气不错) > P(今错不天天气) 、P(I have a dream) > P(I is are boy)
语言模型用于计算文本的成句概率,模型需要做到对合理的句子给出高分,对不合理的句子给出低分
二、语言模型的主要用途
1.语言模型 —— 语音识别ASR
语音识别:声音 ——> 文本
声音本质是一种波,将波按时间段切分很多帧,如25ms一段
之后进行声学特征提取,将每一帧转化成一个向量
以声学特征提取后的向量为输入,经过声学模型,预测得到音素
音素与拼音类似,但要考虑声调
音素序列对应多条文本序列,由语言模型挑选出成句概率最高的序列
使用 beam search 或 维特比 的方式解码
例:
语音识别的大致流程
2.语言模型 —— 手写识别OCR
识别模型将图片中文字转化为候选汉字(一般分为定位和识别两步),再由语言模型挑选出成句概率最高的序列,判断候选汉字是否是常见词语的组合,并给定一个分数,根据分数高低对手写文字进行判断
例:
3.语言模型 —— 输入法
输入即为拼音序列,每个拼音自然的有多个候选汉字,根据语言模型挑选高概率序列
输入法是一个细节繁多的任务,在语言模型这一基础算法上,需要考虑常见的打字手误,常见误读,拼音缩略,中英混杂,输出符号,用户习惯等能力
手写输入法,语音输入法同理
三、语言模型的分类
1.统计语言模型 (SLM)
S = Statistics
ngram语言模型等
2.神经语言模型 (NLM)
N = Neural
rnn语言模型等
3.预训练语言模型 (PLM)
P = Pre-train
Bert、GPT等
4.大语言模型(LLM)
L = Large
ChatGPT等
注:从上到下,由简单到复杂
四、语言模型训练方式的分类
1.自回归(auto regressive)语言模型
在训练时由上文预测下文(或反过来)
单向模型,仅使用单侧序列信息
代表:N-gram,ELMO, GPT
2.自编码(auto encoding)语言模型
在训练时预测序列中任意位置的字符
双向模型,吸收上下文信息
代表:BERT
五、N-gram 统计语言模型
N-gram 语言模型是一种基于统计的语言模型,它基于这样一个假设:一个词的出现概率仅与它前面的N-1个词有关
例如:在一个句子中,对于一个 3-gram(也称为 trigram)模型,第 n 个词的出现概率可以表示为:P(w„ | w„-2, w„-1),即第 n 个词在已知其前两个词的条件下的出现概率。
1.如何计算成句概率
计算成句概率 等价于 计算字/词按特定顺序出现的概率(条件概率)
例:
① 以字为单位
P(今天天气不错) = P(今) * P(天|今) * P(天|今天) * P(气|今天天) * P(不|今天天气) * P(错|今天天气不)
② 以词为单位
P(今天 天气 不错) = P(今天)*P(天气 | 今天) *P(不错 | 今天 天气)
③ 如何计算P(今天)?
P(今天) = Count(今天) / Count_total Count_total:语料总词数
P(天气 | 今天) = Count(今天 天气) / Count(今天)
P(不错 | 今天 天气) = Count(今天 天气 不错) / Count(今天 天气)
二元组:今天 天气 2 gram(多元组出现的次数)
三元组:今天 天气 不错 3 gram(多元组出现的次数)
困难:
句子太多了,对任意一门语言,N-gram数量都非常庞大,无法穷举,需要简化
2.N-gram简化 —— 马尔可夫假设:
P(wn|w1,…,wn-1) ≈ P(wn|wn-3,wn-2,wn-1)
假设第n个词出现的概率,仅受其前面有限个词影响
例:P(今天天气不错) = P(今) * P(天|今) * P(天|今天) * P(气|天天) * P(不|天气) * P(错|气不)
Ⅰ 马尔可夫假设的缺陷:
1.影响第n个词的因素可能出现在前面很远的地方(long distance dependency)
例:
我读过关于马尔科夫的生平的书
我看过关于马尔科夫的生平的电影
我听过关于马尔科夫的生平的故事
2.影响第n个词的因素可能出现在其后面
3.影响第n个词的因素可能并不在文中
但是,马尔科夫假设下依然可以得到非常有效的模型
N-gram中的N越大,统计前几个词越多,需要的语料越大,预测的越准确
例:
3.如何给出语料中没出现过的词或N-gram概率?
Ⅰ 平滑问题(smoothing)
理论上说,任意的词组合成的句子,概率都不应当为零
如何给没见过的词或ngram分配概率即为平滑问题,也称折扣问题(discounting)
Ⅱ 平滑方法
1.回退(backoff)
当多元组在词表中不存在如何处理?
当三元组a b c不存在时,退而寻找b c二元组的概率
P(c | a b) = P(c | b) * Bow(ab)【回退概率,一般设置为一个小于一的常数:0.4(谷歌开源方案中给出)】
Bow(ab):称为二元组a b的回退概率
如果ab出现的次数多, 而abc却没有出现过,则我们应该对ab大力惩罚,Bow(ab)的值应该很小
回退概率有很多计算方式,甚至可以设定为常数
回退可以迭代进行
如:序列 a b c d
P(d | a b c) = P(d | b c) * Bow(abc)
P(d | b c) = P(d | c) * Bow(bc)
P(d | c ) = P(d) * Bow(c)
2.加1平滑(add-one smooth)
最终单独的词不存在如何处理?
对于单独的词 1gram 的概率:
Count(total word):是语料的大小(语料中的词数)
V:是词表大小(词表中的词数),词表大小不受语料大小的影响
对于高阶概率同样可以运用加1平滑:
3.<UNK>
出现词表外的词如何处理?
将低频词转化为<UNK>
预测中遇到的未见过的词,也用<UNK>代替
例:一语成谶 ——> 一语成<UNK>
P(<UNK> | 一 语 成)
这是一种 nlp处理未登录词(OOV) 的常见方法
4.插值
受到回退平滑的启发,在计算高阶ngram概率的同时考虑低阶的ngram概率值,以插值给出最终结果
实践证明,这种方式效果有提升
λ 可以在验证集上调参确定
4.语言模型的评价指标
困惑度 perplexity:PPL
一种PPL:成句概率的倒数开n次根号
PPL值与成句概率成反比
另一种PPL:用对数求和代替小数乘积
本质是相同的,与成句概率成反比
思考:PPL越小,语言模型效果越好,这一结论是否正确?
成句概率是个相对值
PPL不是评价语言模型效果的唯一指标
我们一般使用一些一定合理的目标文本来计算PPL,若PPL值低,则说明成句概率高,也就说明由此语言模型来判断,该句子的合理性高,这样是一个好的语言模型,此语言模型效果还可以
通常来讲,我们需要用语言模型的下游任务,一件具体的事来评价语言模型的效果好坏
5.N-gram语言模型代码实现
Ⅰ 初始化类
① 初始化参数和属性:
n:设定n-gram模型的阶数,默认为3
sep、sos、eos:定义分隔符、句子开始和结束标识符
unk_prob、fix_backoff_prob:设置未知词概率和回退概率
② 创建两个字典:
ngram_count_dict:用于存储n-gram的频率统计
ngram_count_prob_dict:用于存储n-gram的概率
③ 调用方法:
ngram_count(corpus):统计语料库中的n-gram频率
calc_ngram_prob():计算n-gram的概率
dict():Python 内置的字典类型构造函数,用于创建键值对集合。
参数形式 | 是否必需 | 默认值 | 描述 |
---|---|---|---|
无参数 | 否 | 空字典 | 创建空字典 |
mapping | 否 | - | 接受另一个字典或映射对象(如 collections.ChainMap ) |
iterable | 否 | - | 可迭代对象,元素为 (key, value) 元组 |
**kwargs | 否 | - | 关键字参数,如 dict(a=1, b=2) |
defaultdict():collections
模块中的类,是 dict
的子类,自动为不存在的键生成默认值。
参数 | 是否必需 | 默认值 | 描述 |
---|---|---|---|
default_factory | 是 | None | 工厂函数,用于生成缺失键的默认值(如 int , list , lambda ) |
其他参数 | 否 | - | 同 dict() 的初始化参数(映射、可迭代对象、关键字参数) |
range(): 生成一个不可变的整数序列(常用于循环或列表生成)。
参数形式 | 是否必需 | 默认值 | 描述 |
---|---|---|---|
range(stop) | stop 必需 | - | 生成 0 到 stop-1 的整数序列 |
range(start, stop) | start 可选 | start=0 | 生成 start 到 stop-1 的整数序列 |
range(start, stop, step) | step 可选 | step=1 | 生成 start 开始,步长为 step 的序列 |
def __init__(self, corpus=None, n=3):
self.n = n
self.sep = "_" # 用来分割两个词,没有实际含义,只要是字典里不存在的符号都可以
self.sos = "<sos>" #start of sentence,句子开始的标识符
self.eos = "<eos>" #end of sentence,句子结束的标识符
self.unk_prob = 1e-5 #给unk分配一个比较小的概率值,避免集外词概率为0
self.fix_backoff_prob = 0.4 #使用固定的回退概率
self.ngram_count_dict = dict((x + 1, defaultdict(int)) for x in range(n))
self.ngram_count_prob_dict = dict((x + 1, defaultdict(int)) for x in range(n))
self.ngram_count(corpus)
self.calc_ngram_prob()
Ⅱ 将文本切分成词或字或token
split():Python 字符串对象的内置方法,用于将一个字符串按照指定的分隔符分割成多个子字符串,并返回一个包含这些子字符串的列表。如果不指定分隔符,默认使用空白字符(如空格、制表符、换行符等)作为分隔符。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
sep | str | None | 否 | 用于指定分割字符串的分隔符。如果不提供该参数,则默认使用空白字符作为分隔符。 |
maxsplit | int | -1 | 否 | 最大分割次数。如果指定为 -1 或不指定,则进行尽可能多的分割;若指定为正整数 n ,则最多分割 n 次。 |
jieba.lcut(): jieba
库中的一个函数,jieba
是一个强大的中文分词库,lcut()
用于对中文文本进行分词操作,将中文句子分割成一个个有意义的词语,并返回一个包含这些词语的列表。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
sentence | str | 无 | 是 | 要进行分词的中文句子。 |
cut_all | bool | False | 否 | 分词模式选择。False 表示精确模式,试图将句子最精确地切开,适合文本分析;True 表示全模式,把句子中所有可以成词的词语都扫描出来,速度快但可能存在大量冗余。 |
HMM | bool | True | 否 | 是否使用隐马尔可夫模型(HMM)来识别未登录词。 |
#将文本切分成词或字或token
def sentence_segment(self, sentence):
return sentence.split()
#return jieba.lcut(sentence)
Ⅲ 统计ngram的数量
① 遍历语料库
逐句处理语料库中的句子
② 分词并添加边界符
将句子分词,并在前后添加开始符(self.sos)和结束符(self.eos)
③ 按不同窗长扫描
使用不同长度的窗口(从1到self.n)扫描每个句子,生成n-gram
④ 过滤不完整的n-gram
跳过末尾不足窗长的部分
⑤ 存储n-gram
将生成的n-gram用分隔符self.sep连接后存储到字典中,并计数
⑥ 计算总词数
统计所有一阶n-gram的总词数,用于后续概率计算
range(): Python 的内置函数,用于生成一个不可变的整数序列,通常用于在 for
循环中控制迭代次数。它可以根据传入的参数生成指定范围的整数序列。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
start | int | 0 | 否 | 序列的起始值,包含该值。如果不提供,默认从 0 开始。 |
stop | int | 无 | 是 | 序列的结束值,不包含该值。 |
step | int | 1 | 否 | 序列中相邻两个数的差值。如果不提供,默认步长为 1。 |
enumerate():Python 的内置函数,用于将一个可迭代对象(如列表、元组、字符串等)组合为一个索引序列,同时列出数据和数据的索引,一般用于在 for
循环中同时获取元素和其索引。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 无 | 是 | 要进行枚举的可迭代对象。 |
start | int | 0 | 否 | 索引的起始值。如果不提供,默认从 0 开始。 |
join():字符串对象的方法,用于将一个可迭代对象中的元素连接成一个字符串,连接时使用调用该方法的字符串作为分隔符。可迭代对象中的元素必须是字符串类型。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等),元素为字符串 | 无 | 是 | 要连接的可迭代对象。 |
values():字典(dict
)对象的方法,用于返回一个视图对象,该视图对象包含字典中所有的值。这个视图对象会随着字典的变化而动态更新。
#统计ngram的数量
def ngram_count(self, corpus):
for sentence in corpus:
word_lists = self.sentence_segment(sentence)
word_lists = [self.sos] + word_lists + [self.eos] #前后补充开始符和结尾符
for window_size in range(1, self.n + 1): #按不同窗长扫描文本
for index, word in enumerate(word_lists):
#取到末尾时窗口长度会小于指定的gram,跳过那几个
if len(word_lists[index:index + window_size]) != window_size:
continue
#用分隔符连接word形成一个ngram用于存储
ngram = self.sep.join(word_lists[index:index + window_size])
self.ngram_count_dict[window_size][ngram] += 1
#计算总词数,后续用于计算一阶ngram概率
self.ngram_count_dict[0] = sum(self.ngram_count_dict[1].values())
return
Ⅳ 计算ngram的概率
① 遍历窗口大小
从1到self.n,逐个处理不同长度的N-gram
② 遍历N-gram
对于每个窗口大小,遍历对应的N-gram及其出现次数
③ 处理前缀
如果窗口大小大于1,将N-gram拆分为前缀和最后一个词,并获取前缀的计数
如果窗口大小为1,直接使用总词数作为分母
④ 计算概率
将当前N-gram的计数除以前缀的计数,存储在self.ngram_count_prob_dict中。
split():Python 字符串对象的内置方法,用于将一个字符串按照指定的分隔符分割成多个子字符串,并返回一个包含这些子字符串的列表。如果不指定分隔符,默认使用空白字符(如空格、制表符、换行符等)作为分隔符。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
sep | str | None | 否 | 用于指定分割字符串的分隔符。如果不提供该参数,则默认使用空白字符作为分隔符。 |
maxsplit | int | -1 | 否 | 最大分割次数。如果指定为 -1 或不指定,则进行尽可能多的分割;若指定为正整数 n ,则最多分割 n 次。 |
join():字符串对象的方法,用于将一个可迭代对象中的元素连接成一个字符串,连接时使用调用该方法的字符串作为分隔符。可迭代对象中的元素必须是字符串类型。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等),元素为字符串 | 无 | 是 | 要连接的可迭代对象。 |
#计算ngram概率
def calc_ngram_prob(self):
for window_size in range(1, self.n + 1):
for ngram, count in self.ngram_count_dict[window_size].items():
if window_size > 1:
ngram_splits = ngram.split(self.sep) #Class1_ngram :a b c
ngram_prefix = self.sep.join(ngram_splits[:-1]) #ngram_prefix :a b
ngram_prefix_count = self.ngram_count_dict[window_size - 1][ngram_prefix] #Count(a,b)
else:
ngram_prefix_count = self.ngram_count_dict[0] #count(total word)
# word = ngram_splits[-1]
# self.ngram_count_prob_dict[word + "|" + ngram_prefix] = count / ngram_prefix_count
self.ngram_count_prob_dict[window_size][ngram] = count / ngram_prefix_count
return
Ⅴ 获取ngram概率
① 计算n-gram长度
通过self.sep分隔符将n-gram拆分成单词,并计算长度n。
② 查找概率
如果n-gram存在于self.ngram_count_prob_dict[n]中,直接返回其概率。
如果n-gram是一阶(即单个词)且未找到,则返回未知词概率self.unk_prob。
对于高于一阶的n-gram,移除第一个词后递归调用get_ngram_prob,并乘以回退概率self.fix_backoff_prob。
join():字符串对象的方法,用于将一个可迭代对象中的元素连接成一个字符串,连接时使用调用该方法的字符串作为分隔符。可迭代对象中的元素必须是字符串类型。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等),元素为字符串 | 无 | 是 | 要连接的可迭代对象。 |
# 获取ngram概率,其中用到了回退平滑,回退概率采取固定值
def get_ngram_prob(self, ngram):
n = len(ngram.split(self.sep))
if ngram in self.ngram_count_prob_dict[n]:
#尝试直接取出概率
return self.ngram_count_prob_dict[n][ngram]
elif n == 1:
#一阶gram查找不到,说明是集外词,不做回退
return self.unk_prob
else:
#高于一阶的可以回退
ngram = self.sep.join(ngram.split(self.sep)[1:])
return self.fix_backoff_prob * self.get_ngram_prob(ngram)
Ⅵ 回退法预测句子概率
① 分词
将输入句子 sentence 分割成单词列表 word_list。
② 添加边界标记
在单词列表的开头和结尾分别添加起始标记 self.sos 和结束标记 self.eos。
③ 初始化概率
初始化 sentence_prob 为 0,用于累加每个n-gram的概率对数。
④ 遍历单词列表
对于每个单词,构建对应的n-gram,并获取其概率 prob。
⑤ 累加对数概率
将每个n-gram的概率的对数累加到 sentence_prob 中。
⑥ 计算困惑度
根据公式 2 ** (sentence_prob * (-1 / len(word_list))) 计算并返回句子的困惑度
enumerate():Python 的内置函数,用于将一个可迭代对象(如列表、元组、字符串等)组合为一个索引序列,同时列出数据和数据的索引,一般用于在 for
循环中同时获取元素和其索引。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 无 | 是 | 要进行枚举的可迭代对象。 |
start | int | 0 | 否 | 索引的起始值。如果不提供,默认从 0 开始。 |
join():字符串对象的方法,用于将一个可迭代对象中的元素连接成一个字符串,连接时使用调用该方法的字符串作为分隔符。可迭代对象中的元素必须是字符串类型。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等),元素为字符串 | 无 | 是 | 要连接的可迭代对象。 |
math.log():Python 标准库 math
模块中的一个函数,用于计算一个数的自然对数或者以指定底数的对数。自然对数是以常数 (约等于 2.71828)为底的对数。在数学和科学计算中,对数函数是一种常见的运算,math.log()
为我们在 Python 中进行对数计算提供了便利。
参数名 | 参数类型 | 默认值 | 是否必填 | 描述 |
---|---|---|---|---|
x | 数值类型(如 int 、float ) | 无 | 是 | 要计算对数的数值,必须为正数。如果传入负数或零,会抛出 ValueError 异常。 |
base | 数值类型(如 int 、float ) | 无(计算自然对数时) | 否 | 对数的底数。如果不提供该参数,默认计算自然对数(以 为底)。底数必须为正数且不等于 1。 |
#回退法预测句子概率
def calc_sentence_ppl(self, sentence):
word_list = self.sentence_segment(sentence)
word_list = [self.sos] + word_list + [self.eos]
sentence_prob = 0
for index, word in enumerate(word_list):
ngram = self.sep.join(word_list[max(0, index - self.n + 1):index + 1])
prob = self.get_ngram_prob(ngram)
# print(Class1_ngram, prob)
sentence_prob += math.log(prob)
return 2 ** (sentence_prob * (-1 / len(word_list)))
Ⅶ 实现n-gram语言模型
import math
from collections import defaultdict
class NgramLanguageModel:
def __init__(self, corpus=None, n=3):
self.n = n
self.sep = "_" # 用来分割两个词,没有实际含义,只要是字典里不存在的符号都可以
self.sos = "<sos>" #start of sentence,句子开始的标识符
self.eos = "<eos>" #end of sentence,句子结束的标识符
self.unk_prob = 1e-5 #给unk分配一个比较小的概率值,避免集外词概率为0
self.fix_backoff_prob = 0.4 #使用固定的回退概率
self.ngram_count_dict = dict((x + 1, defaultdict(int)) for x in range(n))
self.ngram_count_prob_dict = dict((x + 1, defaultdict(int)) for x in range(n))
self.ngram_count(corpus)
self.calc_ngram_prob()
#将文本切分成词或字或token
def sentence_segment(self, sentence):
return sentence.split()
#return jieba.lcut(sentence)
#统计ngram的数量
def ngram_count(self, corpus):
for sentence in corpus:
word_lists = self.sentence_segment(sentence)
word_lists = [self.sos] + word_lists + [self.eos] #前后补充开始符和结尾符
for window_size in range(1, self.n + 1): #按不同窗长扫描文本
for index, word in enumerate(word_lists):
#取到末尾时窗口长度会小于指定的gram,跳过那几个
if len(word_lists[index:index + window_size]) != window_size:
continue
#用分隔符连接word形成一个ngram用于存储
ngram = self.sep.join(word_lists[index:index + window_size])
self.ngram_count_dict[window_size][ngram] += 1
#计算总词数,后续用于计算一阶ngram概率
self.ngram_count_dict[0] = sum(self.ngram_count_dict[1].values())
return
#计算ngram概率
def calc_ngram_prob(self):
for window_size in range(1, self.n + 1):
for ngram, count in self.ngram_count_dict[window_size].items():
if window_size > 1:
ngram_splits = ngram.split(self.sep) #Class1_ngram :a b c
ngram_prefix = self.sep.join(ngram_splits[:-1]) #ngram_prefix :a b
ngram_prefix_count = self.ngram_count_dict[window_size - 1][ngram_prefix] #Count(a,b)
else:
ngram_prefix_count = self.ngram_count_dict[0] #count(total word)
# word = ngram_splits[-1]
# self.ngram_count_prob_dict[word + "|" + ngram_prefix] = count / ngram_prefix_count
self.ngram_count_prob_dict[window_size][ngram] = count / ngram_prefix_count
return
#获取ngram概率,其中用到了回退平滑,回退概率采取固定值
def get_ngram_prob(self, ngram):
n = len(ngram.split(self.sep))
if ngram in self.ngram_count_prob_dict[n]:
#尝试直接取出概率
return self.ngram_count_prob_dict[n][ngram]
elif n == 1:
#一阶gram查找不到,说明是集外词,不做回退
return self.unk_prob
else:
#高于一阶的可以回退
ngram = self.sep.join(ngram.split(self.sep)[1:])
return self.fix_backoff_prob * self.get_ngram_prob(ngram)
#回退法预测句子概率
def calc_sentence_ppl(self, sentence):
word_list = self.sentence_segment(sentence)
word_list = [self.sos] + word_list + [self.eos]
sentence_prob = 0
for index, word in enumerate(word_list):
ngram = self.sep.join(word_list[max(0, index - self.n + 1):index + 1])
prob = self.get_ngram_prob(ngram)
# print(Class1_ngram, prob)
sentence_prob += math.log(prob)
return 2 ** (sentence_prob * (-1 / len(word_list)))
if __name__ == "__main__":
corpus = open("sample.txt", encoding="utf8").readlines()
lm = NgramLanguageModel(corpus, 3)
print("词总数:", lm.ngram_count_dict[0])
print(lm.ngram_count_prob_dict)
print(lm.calc_sentence_ppl("c d b d b"))
⭐七、NN语言模型:神经网络语言模型
Bengio et al. 2003
与ngram模型相似使用,前n个词预测下一个词
输出在字表上的概率分布
语言模型训练得到了词向量这一副产品
随着相关研究的发展,隐含层模型结构的复杂度不断提升
DNN ——> CNN / RNN ——> LSTM / GRU ——> transformer
LSTM示意图
1.代码实现:RNN语言模型
Ⅰ、模型搭建
nn.Embedding():PyTorch 中用于创建词嵌入层的模块。在自然语言处理任务中,我们通常需要将离散的词转换为连续的向量表示,这个过程就叫做词嵌入。nn.Embedding()
模块可以学习一个词嵌入矩阵,将输入的词索引映射到对应的词向量。
参数名 | 参数类型 | 描述 |
---|---|---|
num_embeddings | int | 词表的大小,即不同词的数量。 |
embedding_dim | int | 每个词向量的维度。 |
padding_idx | int, 可选 | 用于指定填充的词索引,填充位置的向量会被固定为零向量。 |
max_norm | float, 可选 | 词向量的最大范数,如果超过该值会进行归一化。 |
norm_type | float, 可选 | 计算范数时使用的范数类型,默认为 2 范数。 |
scale_grad_by_freq | bool, 可选 | 是否根据词频缩放梯度。 |
sparse | bool, 可选 | 是否使用稀疏梯度更新。 |
nn.RNN():PyTorch 中用于创建循环神经网络(RNN)层的模块。RNN 是一种能够处理序列数据的神经网络,它可以在不同的时间步之间传递信息,从而捕捉序列中的时间依赖关系。
参数名 | 参数类型 | 描述 |
---|---|---|
input_size | int | 输入序列的特征维度。 |
hidden_size | int | 隐藏状态的维度。 |
num_layers | int, 可选 | RNN 层的数量,默认为 1。 |
nonlinearity | str, 可选 | 非线性激活函数,可选值为 'tanh' 或 'relu' ,默认为 'tanh' 。 |
bias | bool, 可选 | 是否使用偏置项,默认为 True 。 |
batch_first | bool, 可选 | 输入和输出张量的形状是否以批量大小作为第一维,默认为 False 。 |
dropout | float, 可选 | 除最后一层外,每层的丢弃率,范围在 0 到 1 之间,默认为 0。 |
bidirectional | bool, 可选 | 是否使用双向 RNN,默认为 False 。 |
nn.Linear():PyTorch 中用于创建全连接层(线性层)的模块。全连接层将输入的每个神经元与输出的每个神经元都进行连接,通过学习一组权重和偏置来实现线性变换。
参数名 | 参数类型 | 描述 |
---|---|---|
in_features | int | 输入特征的维度。 |
out_features | int | 输出特征的维度。 |
bias | bool, 可选 | 是否使用偏置项,默认为 True 。 |
nn.Dropout():PyTorch 中用于防止过拟合的模块。在训练过程中,Dropout 会以一定的概率随机丢弃(置为零)输入张量中的某些元素,这样可以迫使模型学习到更鲁棒的特征,减少对某些特定神经元的依赖。
参数名 | 参数类型 | 描述 |
---|---|---|
p | float, 可选 | 丢弃元素的概率,范围在 0 到 1 之间,默认为 0.5。 |
inplace | bool, 可选 | 是否原地操作,默认为 False 。 |
nn.functional.cross_entropy():PyTorch 中用于计算交叉熵损失的函数。交叉熵损失常用于分类任务,它衡量了模型预测的概率分布与真实标签的概率分布之间的差异。
参数名 | 参数类型 | 描述 |
---|---|---|
input | torch.Tensor | 模型的预测输出,形状为 (批量大小,类别数)。 |
target | torch.Tensor | 真实标签,形状为 (批量大小),每个元素是对应的类别索引。 |
weight | torch.Tensor, 可选 | 每个类别的权重,用于处理类别不平衡问题。 |
size_average | bool, 可选 | 已弃用,使用 reduction 参数代替。 |
ignore_index | int, 可选 | 忽略的标签索引,这些标签不会参与损失计算,默认为 -100。 |
reduce | bool, 可选 | 已弃用,使用 reduction 参数代替。 |
reduction | str, 可选 | 损失的缩减方式,可选值为 'none' 、'mean' 或 'sum' ,默认为 'mean' 。 |
class LanguageModel(nn.Module):
def __init__(self, input_dim, vocab):
super(LanguageModel, self).__init__()
self.embedding = nn.Embedding(len(vocab) + 1, input_dim)
self.layer = nn.RNN(input_dim, input_dim, num_layers=2, batch_first=True)
self.classify = nn.Linear(input_dim, len(vocab) + 1)
self.dropout = nn.Dropout(0.1)
self.loss = nn.functional.cross_entropy
Ⅱ、前馈运算
torch.softmax():PyTorch 中的一个函数,主要用于将输入张量的元素转换为概率分布。在深度学习的分类任务中,模型的输出往往是一组未经归一化的得分(logits),torch.softmax()
可以将这些得分转换为概率值,使得每个类别的概率值在 0 到 1 之间,并且所有类别的概率值之和为 1。这样可以方便地根据概率值确定输入样本属于各个类别的可能性大小。
参数名 | 参数类型 | 描述 |
---|---|---|
input | torch.Tensor | 输入的张量,通常是模型的输出 logits。 |
dim | int | 指定在哪个维度上进行 softmax 操作。该维度上的元素会被转换为概率分布。例如,对于二维张量,如果 dim = 0 ,则按列进行 softmax 操作;如果 dim = 1 ,则按行进行 softmax 操作。 |
_stacklevel | int | 内部使用的参数,一般不需要用户手动指定。 |
dtype | torch.dtype, 可选 | 指定输出张量的数据类型。如果不指定,输出的数据类型将与输入保持一致。 |
x = x[:, -1, :] :
- 第一个
:
:表示选择所有的第一维元素。这里的第一维通常对应批量大小(在深度学习的批量训练中)或者矩阵的行(对于二维矩阵)。 -1
:表示选择第二维的最后一个元素。在不同的应用场景下,第二维可能代表时间步(在处理序列数据时)、特征维度的某个特定位置等。- 最后一个
:
:表示选择所有的第三维元素。第三维可以代表特征的不同分量等。
#当输入真实标签,返回loss值;无真实标签,返回预测值
def forward(self, x, y=None):
x = self.embedding(x) #output shape:(batch_size, sen_len, input_dim)
x, _ = self.layer(x) #output shape:(batch_size, sen_len, input_dim)
x = x[:, -1, :] #output shape:(batch_size, input_dim)
x = self.dropout(x)
y_pred = self.classify(x) #output shape:(batch_size, vocab_size)
if y is not None:
return self.loss(y_pred, y) #[1*vocab_size] []
else:
return torch.softmax(y_pred, dim=-1)
Ⅲ、获取字符集
set():Python 的内置函数,用于创建一个集合对象。集合是一种无序且元素唯一的数据结构,它可以用来去除重复元素,还能进行集合运算,如交集、并集、差集等。
参数 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 否 | 用于初始化集合的可迭代对象,若不提供则创建空集合 |
open():Python 的内置函数,用于打开文件并返回一个文件对象。借助这个文件对象,我们可以对文件进行读取、写入、追加等操作。
参数 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
file | 字符串 | 是 | 要打开的文件的路径,可为相对或绝对路径 |
mode | 字符串 | 否 | 打开文件的模式,常见有'r' (只读,默认)、'w' (写入)、'a' (追加)、'b' (二进制模式)等 |
buffering | 整数 | 否 | 设置缓冲策略,-1 表示使用默认缓冲 |
encoding | 字符串 | 否 | 文件的编码格式,如'utf-8' 、'gbk' 等,用于文本文件 |
errors | 字符串 | 否 | 指定如何处理编码错误 |
newline | 字符串 | 否 | 控制通用换行模式的行为 |
closefd | 布尔值 | 否 | 如果为True (默认),则在文件关闭时关闭文件描述符 |
opener | 可调用对象 | 否 | 自定义开启文件的方式 |
enumerate():Python 的内置函数,它能将一个可迭代对象组合成一个索引序列,在循环中可以同时获取元素及其索引。
参数 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 是 | 要进行枚举的可迭代对象 |
start | 整数 | 否 | 索引的起始值,默认为 0 |
add():集合(set
)对象的方法,用于向集合中添加一个元素。如果该元素已经存在于集合中,集合不会发生改变,因为集合中的元素具有唯一性。
参数 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
element | 可哈希对象(如数字、字符串、元组等) | 是 | 要添加到集合中的元素 |
sorted():Python 的内置函数,用于对可迭代对象进行排序,并返回一个新的已排序列表,原可迭代对象不会被修改。
参数 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
iterable | 可迭代对象(如列表、元组、字符串等) | 是 | 要进行排序的可迭代对象 |
key | 函数 | 否 | 指定排序依据的函数,该函数接受一个元素作为输入,返回用于比较的值 |
reverse | 布尔值 | 否 | False 表示升序(默认),True 表示降序 |
#读取语料获得字符集
#输出一份
def build_vocab_from_corpus(path):
vocab = set()
with open(path, encoding="utf8") as f:
for index, char in enumerate(f.read()):
vocab.add(char)
vocab.add("<UNK>") #增加一个unk token用来处理未登录词
writer = open("vocab.txt", "w", encoding="utf8")
for char in sorted(vocab):
writer.write(char + "\n")
return vocab
Ⅳ、加载字表
#加载字表
def build_vocab(vocab_path):
vocab = {}
with open(vocab_path, encoding="utf8") as f:
for index, line in enumerate(f):
char = line[:-1] #去掉结尾换行符
vocab[char] = index + 1 #留出0位给pad token
vocab["\n"] = 1
return vocab
Ⅴ、 加载语料
read():从文件对象中读取指定数量的字符(文本模式)或字节(二进制模式),并将其作为字符串(文本模式)或字节对象(二进制模式)返回。若不指定读取的数量,它会尝试读取文件的全部内容。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
size | int | 否 | 可选参数,指定要读取的字符(文本模式)或字节(二进制模式)的数量。若省略该参数或传入 -1 ,则会读取文件的全部内容。 |
#加载语料
def load_corpus(path):
return open(path, encoding="utf8").read()
Ⅵ、生成样本
random.randint():Python 标准库 random
模块中的一个函数,用于生成指定范围内的随机整数。该函数在需要随机选取整数的场景中非常有用,比如随机抽奖、随机生成测试数据等。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
a | int | 是 | 随机整数范围的下限,生成的随机整数会大于等于这个值。 |
b | int | 是 | 随机整数范围的上限,生成的随机整数会小于等于这个值。且要求 b >= a 。 |
get():Python 字典(dict
)对象的一个方法,用于根据键获取字典中对应的值。与直接使用方括号 []
访问键值不同的是,当键不存在时,get()
方法不会抛出 KeyError
异常,而是返回一个默认值。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
key | 任意可哈希类型(如 int 、str 、tuple 等) | 是 | 要查找的键。 |
default | 任意类型 | 否 | 当键不存在时返回的默认值,默认为 None 。 |
#随机生成一个样本
#从文本中截取随机窗口,前n个字作为输入,最后一个字作为输出
def build_sample(vocab, window_size, corpus):
start = random.randint(0, len(corpus) - 1 - window_size)
end = start + window_size
window = corpus[start:end]
target = corpus[end]
# print(window, target)
x = [vocab.get(word, vocab["<UNK>"]) for word in window] #将字转换成序号
y = vocab[target]
return x, y
Ⅶ、建立数据集和模型
range(): Python 的内置函数,用于生成一个不可变的整数序列,通常用于 for
循环中控制迭代次数,也可用于创建列表等可迭代对象。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
start | int | 否(形式一可不填) | 序列的起始值,包含该值。如果不提供,默认从 0 开始。 |
stop | int | 是 | 序列的结束值,不包含该值。 |
step | int | 否(形式一、二可不填) | 序列中相邻两个数的差值。如果不提供,默认步长为 1。 |
append():Python 列表(list
)对象的方法,用于在列表的末尾添加一个元素,从而改变原列表的内容。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
object | 任意类型 | 是 | 要添加到列表末尾的元素,可以是数字、字符串、列表、元组等任意 Python 对象。 |
LongTensor():PyTorch 库中的一个函数,用于创建一个 torch.Tensor
对象,其数据类型为 64 位有符号整数(torch.long
)。在深度学习中,常用来存储整数类型的数据,如索引、标签等。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
data | array_like(如列表、元组、NumPy 数组等) | 否 | 用于初始化张量的数据。如果不提供,将创建一个空张量。 |
dtype | torch.dtype | 否 | 指定张量的数据类型,通常不需要手动指定,默认会创建 torch.long 类型的张量。 |
device | torch.device | 否 | 指定张量存储的设备,如 'cpu' 或 'cuda' (用于 GPU 计算)。如果不指定,默认存储在 CPU 上。 |
requires_grad | bool | 否 | 指定是否需要对该张量进行梯度计算。如果为 True ,在后续的计算中会跟踪梯度信息,用于反向传播。默认值为 False 。 |
#建立数据集
#sample_length 输入需要的样本数量。需要多少生成多少
#vocab 词表
#window_size 样本长度
#corpus 语料字符串
def build_dataset(sample_length, vocab, window_size, corpus):
dataset_x = []
dataset_y = []
for i in range(sample_length):
x, y = build_sample(vocab, window_size, corpus)
dataset_x.append(x)
dataset_y.append(y)
return torch.LongTensor(dataset_x), torch.LongTensor(dataset_y)
#建立模型
def build_model(vocab, char_dim):
model = LanguageModel(char_dim, vocab)
return model
Ⅷ、计算文本ppl
model.eval():PyTorch 中模型对象的一个方法,主要用于将模型设置为评估模式。在深度学习模型的训练和评估过程中,有些层(如 Dropout
、BatchNorm
等)在训练和评估时的行为是不同的。调用 model.eval()
可以确保这些层在评估时使用正确的模式,避免在评估阶段引入不必要的随机性或统计信息更新。
torch.no_grad():PyTorch 中的一个上下文管理器,用于临时禁用梯度计算。在模型评估阶段或者不需要计算梯度的场景(如推理过程)中,禁用梯度计算可以减少内存消耗,提高计算速度。
max():返回可迭代对象中的最大值,或者返回多个参数中的最大值。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
iterable | 可迭代对象(如列表、元组等) | 是(第一种形式) | 要查找最大值的可迭代对象。 |
arg1, arg2, *args | 任意可比较类型 | 是(第二种形式) | 多个要比较的参数。 |
key | 函数 | 否 | 一个用于自定义比较规则的函数,该函数接受一个元素作为输入,返回一个用于比较的值。 |
default | 任意类型 | 否 | 当可迭代对象为空时返回的默认值。 |
torch.cuda.is_available():PyTorch 中的一个函数,用于检查当前环境中是否有可用的 CUDA 设备(即 GPU)。如果返回 True
,表示可以使用 GPU 进行计算;如果返回 False
,则只能使用 CPU 进行计算。
#计算文本ppl
def calc_perplexity(sentence, model, vocab, window_size):
prob = 0
model.eval()
with torch.no_grad():
for i in range(1, len(sentence)):
start = max(0, i - window_size)
window = sentence[start:i]
x = [vocab.get(char, vocab["<UNK>"]) for char in window]
x = torch.LongTensor([x])
target = sentence[i]
target_index = vocab.get(target, vocab["<UNK>"])
if torch.cuda.is_available():
x = x.cuda()
pred_prob_distribute = model(x)[0]
target_prob = pred_prob_distribute[target_index]
prob += math.log(target_prob, 10)
return 2 ** (prob * ( -1 / len(sentence)))
Ⅸ、模型训练
torch.cuda.is_available():PyTorch 中的一个函数,用于检查当前环境中是否有可用的 CUDA 设备(即 GPU)。如果返回 True
,表示可以使用 GPU 进行计算;如果返回 False
,则只能使用 CPU 进行计算。
model.cuda():PyTorch 中用于将模型参数和缓冲区移动到 CUDA 设备(GPU)上的方法。在深度学习中,使用 GPU 进行计算可以显著加速模型的训练和推理过程,因为 GPU 具有强大的并行计算能力。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
device | torch.device 或 int | 否 | 指定要将模型移动到的 CUDA 设备。如果不提供该参数,默认使用当前的 CUDA 设备。 |
torch.optim.Adam():PyTorch 中实现 Adam(Adaptive Moment Estimation)优化算法的类。Adam 是一种常用的优化算法,结合了动量法和自适应学习率的思想,能够自适应地调整每个参数的学习率,具有收敛速度快、稳定性好的优点。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
params | 可迭代对象 | 是 | 包含模型参数的可迭代对象,通常使用 model.parameters() 来获取。 |
lr | float | 否 | 学习率,控制参数更新的步长,默认值为 0.001。 |
betas | tuple | 否 | 用于计算梯度一阶矩估计和二阶矩估计的指数衰减率,默认值为 (0.9, 0.999) 。 |
eps | float | 否 | 用于数值稳定性的小常数,防止分母为零,默认值为 1e-08 。 |
weight_decay | float | 否 | 权重衰减系数,用于正则化,防止过拟合,默认值为 0。 |
amsgrad | bool | 否 | 是否使用 AMSGrad 变种的 Adam 算法,默认值为 False 。 |
model.train():PyTorch 中用于将模型设置为训练模式的方法。在训练模式下,一些特殊层(如 Dropout
、BatchNorm
等)会按照训练时的规则进行操作,例如 Dropout
会随机丢弃一些神经元,BatchNorm
会更新统计信息。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
mode | bool | 否 | 指定是否将模型设置为训练模式,默认值为 True 。如果设置为 False ,则等同于 model.eval() 。 |
optim.zero_grad(): PyTorch 中优化器对象的方法,用于将模型参数的梯度清零。在每次进行反向传播计算梯度之前,需要将之前的梯度清零,以避免梯度累积。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
set_to_none | bool | 否 | 是否将梯度设置为 None 而不是零,默认值为 False 。将梯度设置为 None 可以减少内存占用。 |
backward():PyTorch 中张量对象的方法,用于进行反向传播计算梯度。在计算图中,通过调用 backward()
方法,可以根据损失函数的值计算出模型参数的梯度。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
gradient | torch.Tensor | 否 | 梯度张量,用于非标量张量的反向传播。如果 tensor 是标量,则不需要提供该参数。 |
retain_graph | bool | 否 | 是否保留计算图,默认值为 None 。如果需要多次调用 backward() ,则需要将该参数设置为 True 。 |
create_graph | bool | 否 | 是否创建计算图以进行高阶导数计算,默认值为 False 。 |
inputs | 可迭代对象 | 否 | 指定要计算梯度的输入张量。 |
optim.step(): PyTorch 中优化器对象的方法,用于根据计算得到的梯度更新模型的参数。在调用 backward()
计算梯度后,需要调用 optim.step()
来更新参数。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
closure | 可调用对象 | 否 | 一个闭包函数,用于重新评估模型并返回损失,通常用于一些需要多次评估的优化算法,默认值为 None 。 |
os.listdir(): Python 标准库 os
模块中的函数,用于返回指定目录下的所有文件和文件夹的名称列表。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
path | 字符串 | 否 | 指定要列出内容的目录路径,默认值为当前工作目录(. )。 |
def train(corpus_path, save_weight=True):
epoch_num = 10 #训练轮数
batch_size = 128 #每次训练样本个数
train_sample = 10000 #每轮训练总共训练的样本总数
char_dim = 128 #每个字的维度
window_size = 6 #样本文本长度
vocab = build_vocab("vocab.txt") #建立字表
corpus = load_corpus(corpus_path) #加载语料
model = build_model(vocab, char_dim) #建立模型
if torch.cuda.is_available():
model = model.cuda()
optim = torch.optim.Adam(model.parameters(), lr=0.001) #建立优化器
for epoch in range(epoch_num):
model.train()
watch_loss = []
for batch in range(int(train_sample / batch_size)):
x, y = build_dataset(batch_size, vocab, window_size, corpus) #构建一组训练样本
if torch.cuda.is_available():
x, y = x.cuda(), y.cuda()
optim.zero_grad() #梯度归零
loss = model(x, y) #计算loss
watch_loss.append(loss.item())
loss.backward() #计算梯度
optim.step() #更新权重
print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
if not save_weight:
return
else:
base_name = os.path.basename(corpus_path).replace("txt", "pth")
model_path = os.path.join("model", base_name)
torch.save(model.state_dict(), model_path)
return
#训练corpus文件夹下的所有语料,根据文件名将训练后的模型放到莫得了文件夹
def train_all():
for path in os.listdir("corpus"):
corpus_path = os.path.join("corpus", path)
train(corpus_path)
Ⅹ、基于pytorch的rnn语言模型
#coding:utf8
import torch
import torch.nn as nn
import numpy as np
import math
import random
import os
import re
import matplotlib.pyplot as plt
"""
基于pytorch的rnn语言模型
"""
class LanguageModel(nn.Module):
def __init__(self, input_dim, vocab):
super(LanguageModel, self).__init__()
self.embedding = nn.Embedding(len(vocab) + 1, input_dim)
self.layer = nn.RNN(input_dim, input_dim, num_layers=2, batch_first=True)
self.classify = nn.Linear(input_dim, len(vocab) + 1)
self.dropout = nn.Dropout(0.1)
self.loss = nn.functional.cross_entropy
#当输入真实标签,返回loss值;无真实标签,返回预测值
def forward(self, x, y=None):
x = self.embedding(x) #output shape:(batch_size, sen_len, input_dim)
x, _ = self.layer(x) #output shape:(batch_size, sen_len, input_dim)
x = x[:, -1, :] #output shape:(batch_size, input_dim)
x = self.dropout(x)
y_pred = self.classify(x) #output shape:(batch_size, vocab_size)
if y is not None:
return self.loss(y_pred, y) #[1*vocab_size] []
else:
return torch.softmax(y_pred, dim=-1)
#读取语料获得字符集
#输出一份
def build_vocab_from_corpus(path):
vocab = set()
with open(path, encoding="utf8") as f:
for index, char in enumerate(f.read()):
vocab.add(char)
vocab.add("<UNK>") #增加一个unk token用来处理未登录词
writer = open("vocab.txt", "w", encoding="utf8")
for char in sorted(vocab):
writer.write(char + "\n")
return vocab
#加载字表
def build_vocab(vocab_path):
vocab = {}
with open(vocab_path, encoding="utf8") as f:
for index, line in enumerate(f):
char = line[:-1] #去掉结尾换行符
vocab[char] = index + 1 #留出0位给pad token
vocab["\n"] = 1
return vocab
#加载语料
def load_corpus(path):
return open(path, encoding="utf8").read()
#随机生成一个样本
#从文本中截取随机窗口,前n个字作为输入,最后一个字作为输出
def build_sample(vocab, window_size, corpus):
start = random.randint(0, len(corpus) - 1 - window_size)
end = start + window_size
window = corpus[start:end]
target = corpus[end]
# print(window, target)
x = [vocab.get(word, vocab["<UNK>"]) for word in window] #将字转换成序号
y = vocab[target]
return x, y
#建立数据集
#sample_length 输入需要的样本数量。需要多少生成多少
#vocab 词表
#window_size 样本长度
#corpus 语料字符串
def build_dataset(sample_length, vocab, window_size, corpus):
dataset_x = []
dataset_y = []
for i in range(sample_length):
x, y = build_sample(vocab, window_size, corpus)
dataset_x.append(x)
dataset_y.append(y)
return torch.LongTensor(dataset_x), torch.LongTensor(dataset_y)
#建立模型
def build_model(vocab, char_dim):
model = LanguageModel(char_dim, vocab)
return model
#计算文本ppl
def calc_perplexity(sentence, model, vocab, window_size):
prob = 0
model.eval()
with torch.no_grad():
for i in range(1, len(sentence)):
start = max(0, i - window_size)
window = sentence[start:i]
x = [vocab.get(char, vocab["<UNK>"]) for char in window]
x = torch.LongTensor([x])
target = sentence[i]
target_index = vocab.get(target, vocab["<UNK>"])
if torch.cuda.is_available():
x = x.cuda()
pred_prob_distribute = model(x)[0]
target_prob = pred_prob_distribute[target_index]
prob += math.log(target_prob, 10)
return 2 ** (prob * ( -1 / len(sentence)))
def train(corpus_path, save_weight=True):
epoch_num = 10 #训练轮数
batch_size = 128 #每次训练样本个数
train_sample = 10000 #每轮训练总共训练的样本总数
char_dim = 128 #每个字的维度
window_size = 6 #样本文本长度
vocab = build_vocab("vocab.txt") #建立字表
corpus = load_corpus(corpus_path) #加载语料
model = build_model(vocab, char_dim) #建立模型
if torch.cuda.is_available():
model = model.cuda()
optim = torch.optim.Adam(model.parameters(), lr=0.001) #建立优化器
for epoch in range(epoch_num):
model.train()
watch_loss = []
for batch in range(int(train_sample / batch_size)):
x, y = build_dataset(batch_size, vocab, window_size, corpus) #构建一组训练样本
if torch.cuda.is_available():
x, y = x.cuda(), y.cuda()
optim.zero_grad() #梯度归零
loss = model(x, y) #计算loss
watch_loss.append(loss.item())
loss.backward() #计算梯度
optim.step() #更新权重
print("=========\n第%d轮平均loss:%f" % (epoch + 1, np.mean(watch_loss)))
if not save_weight:
return
else:
base_name = os.path.basename(corpus_path).replace("txt", "pth")
model_path = os.path.join("model", base_name)
torch.save(model.state_dict(), model_path)
return
#训练corpus文件夹下的所有语料,根据文件名将训练后的模型放到莫得了文件夹
def train_all():
for path in os.listdir("corpus"):
corpus_path = os.path.join("corpus", path)
train(corpus_path)
if __name__ == "__main__":
# build_vocab_from_corpus("corpus/all.txt")
# train("corpus.txt", True)
train_all()
2.使用训练好的语言模型作预测
Ⅰ、加载训练好的语言模型
load_state_dict(): PyTorch 中模型对象的一个方法,用于将预训练的模型参数加载到当前模型中。在深度学习中,我们常常会使用预训练模型来加速训练过程或提高模型性能,通过该方法可以方便地将保存的参数应用到新的模型实例上。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
state_dict | 字典 | 是 | 包含模型参数的字典,通常是通过 torch.save() 保存的模型状态字典。 |
strict | 布尔值 | 否 | 指定是否严格匹配模型的状态字典和加载的状态字典。若为 True ,则要求两者的键和形状完全一致;若为 False ,则允许部分匹配,忽略不匹配的键。默认值为 True 。 |
torch.load():PyTorch 中的一个函数,用于从文件中加载保存的对象,如模型的状态字典、整个模型、张量等。它可以处理不同格式的文件,并且支持在 CPU 和 GPU 之间迁移数据。
参数名 | 参数类型 | 是否必填 | 描述 |
---|---|---|---|
f | 字符串或文件对象 | 是 | 要加载的文件路径或文件对象。 |
map_location | 字符串、torch.device 、函数或字典 | 否 | 指定加载数据的设备。可以是 'cpu' 、'cuda' 等,也可以是自定义的映射函数,用于在不同设备之间迁移数据。默认情况下,数据会加载到保存时的设备上。 |
pickle_module | 模块 | 否 | 用于反序列化的 pickle 模块,默认使用 Python 内置的 pickle 模块。 |
**pickle_load_args | 可变关键字参数 | 否 | 传递给 pickle.load() 的其他参数。 |
model.eval():PyTorch 中模型对象的一个方法,用于将模型设置为评估模式。在深度学习模型中,有些层(如 Dropout
、BatchNorm
等)在训练和评估阶段的行为是不同的。例如,Dropout
在训练时会随机丢弃部分神经元以防止过拟合,而在评估时则不进行丢弃操作;BatchNorm
在训练时会更新统计信息,而在评估时使用训练阶段统计好的均值和方差。调用 model.eval()
可以确保这些层在评估阶段使用正确的行为。
def load_trained_language_model(path):
char_dim = 128 #每个字的维度,与训练时保持一直
window_size = 6 #样本文本长度,与训练时保持一直
vocab = build_vocab("vocab.txt") # 加载字表
model = build_model(vocab, char_dim) # 加载模型
model.load_state_dict(torch.load(path)) #加载训练好的模型权重
model.eval()
if torch.cuda.is_available():
model = model.cuda()
model.window_size = window_size
model.vocab = vocab
return model
Ⅱ、使用训练好的语言模型做预测
#coding:utf8
import torch
import torch.nn as nn
import numpy as np
import math
import random
import re
import os
from demo2_nnlm import build_model, build_vocab
"""
使用训练好的语言模型
"""
def load_trained_language_model(path):
char_dim = 128 #每个字的维度,与训练时保持一直
window_size = 6 #样本文本长度,与训练时保持一直
vocab = build_vocab("vocab.txt") # 加载字表
model = build_model(vocab, char_dim) # 加载模型
model.load_state_dict(torch.load(path)) #加载训练好的模型权重
model.eval()
if torch.cuda.is_available():
model = model.cuda()
model.window_size = window_size
model.vocab = vocab
return model
#计算文本ppl
def calc_perplexity(sentence, model):
prob = 0
with torch.no_grad():
for i in range(1, len(sentence)):
start = max(0, i - model.window_size)
window = sentence[start:i]
x = [model.vocab.get(char, model.vocab["<UNK>"]) for char in window]
x = torch.LongTensor([x])
target = sentence[i]
target_index = model.vocab.get(target, model.vocab["<UNK>"])
if torch.cuda.is_available():
x = x.cuda()
pred_prob_distribute = model(x)[0]
target_prob = pred_prob_distribute[target_index]
# print(window , "->", target, "prob:", float(target_prob))
prob += math.log(target_prob, 10)
return 2 ** (prob * ( -1 / len(sentence)))
#加载训练好的所有模型
def load_models():
model_paths = os.listdir(os.path.dirname(os.path.abspath(__file__)) + "/model")
class_to_model = {}
for model_path in model_paths:
class_name = model_path.replace(".pth", "")
model_path = os.path.join("model", model_path)
class_to_model[class_name] = load_trained_language_model(model_path)
return class_to_model
#基于语言模型的文本分类伪代码
#class_to_model: {"class1":<language model obj1>, "class2":<language model obj2>, ..}
#每个语言模型,用对应的领域语料训练
def text_classification_based_on_language_model(class_to_model, sentence):
ppl = []
for class_name, class_lm in class_to_model.items():
#用每个语言模型计算ppl
ppl.append([class_name, calc_perplexity(sentence, class_lm)])
ppl = sorted(ppl, key=lambda x:x[1])
print(sentence)
print(ppl[0: ])
print("==================")
return ppl
sentence = ["在全球货币体系出现危机的情况下",
"点击进入双色球玩法经典选号图表",
"慢时尚服饰最大的优点是独特",
"做处女座朋友的人真的很难",
"网戒中心要求家长全程陪护",
"在欧巡赛扭转了自己此前不利的状态",
"选择独立的别墅会比公寓更适合你",
]
class_to_model = load_models()
for s in sentence:
text_classification_based_on_language_model(class_to_model, s)
八、两类语言模型的对比
NN神经网络语言模型在概率预测时自带平滑,模型可以自动处理非零预测问题,模型体型固定,不受训练语料的影响,不需要寻找回退策略,泛化能力较强
如果模型足够优秀,则神经网络语言模型的长度不用考虑,而N-gram模型随着记录长度的增加,训练数据的增大而增大,神经网络的训练数据不会改变
1.数据稀疏性处理
N-gram 语言模型
稀疏性问题:由于自然语言的复杂性和多样性,在训练数据中很多 N-gram 序列可能不会出现,导致零概率问题。
平滑技术:使用平滑方法(如加一平滑、Kneser-Ney 平滑等)来解决稀疏性问题,但效果有限,可能导致性能下降。
神经网络语言模型:
词嵌入:将词映射到低维向量空间,利用词嵌入的相似性来缓解稀疏性问题,即使在训练数据中没有出现的词组合,也可以通过相似词的信息来估计概率。
泛化能力:神经网络通过训练可以学习到词之间的潜在语义关系,具有更好的泛化能力,对未出现的词组合也能做出相对合理的预测。
2.性能和效率
N-gram 语言模型
计算复杂度:存储和计算 N-gram 计数相对简单,但对于大规模语料库,存储大量的 N-gram 计数需要大量的内存,且计算概率的时间复杂度较高,尤其是在处理长序列时。
可扩展性:在处理大规模数据时,存储和性能会成为瓶颈,难以处理长距离依赖关系
神经网络语言模型
计算复杂度:训练神经网络需要更多的计算资源和时间,但可以通过 GPU 加速。
可扩展性:可以处理大规模数据,能够学习长距离依赖,通过调整网络结构和参数,可以适应不同的任务和数据规模。
3.对上下文的处理
N-gram 语言模型
局部依赖:只能处理有限长度的上下文(N-1个词),难以捕捉长距离依赖关系,例如在处理长句子时由于依赖范围的限制,可能无法准确预测后续词。
神经网络语言模型
长距离依赖:理论上可以通过网络结构(如循环神经网络 RNN、长短时记忆网络 LSTM、门控循环单元GRU 或 Transformer)学习长距离依赖关系,捕捉更远的上下文信息。
4.总结
N-gram 语言模型:简单、易于理解和实现,适合处理小规模数据和简单的语言任务,但受限于数据稀疏性和短距离依赖。
神经网络语言模型:计算成本高但功能强大,通过深度学习技术可以处理大规模数据和复杂的语言现象,能够学习长距离依赖和语义关系,适合高级的自然语言处理任务,但需要更多的计算资源和数据。
九、语言模型的应用
1.话者分离
① 根据说话内容判断说话人
常用于语言识别系统中,判断录音对话中角色
如:客服对话录音,判断坐席或客户
② 根据不同腔调判断说话人
翻译腔: 这倒霉的房子里竟然有蟑螂,你可以想象到吗?这真是太可怕了!
港台腔:你这个人怎么可以这个样子
东北味: 我不稀得说你那些事儿就拉倒了
话者分离本质上为文本分类任务
步骤:
1.对于每个类别,使用类别语料训练语言模型
2.对于一个新输入的文本,用所有语言模型计算成句概率
3.选取概率最高的类别为预测类别
相比一般文本分类模型,如贝叶斯,rf,神经网络等,NN语言模型的优势:
1.每个类别模型互相独立,样本不均衡或样本有错误对其他模型没有影响
2..可以随时增加新的类别,而不影响旧的类别的效果,在加入新的类别时,不需要重新训练模型
效果上讲:一般不会有显著优势 效率上讲:一般会低于统一的分类模型
2.文本纠错
纠正文本中的错误
如:
我今天去了天暗门看人民英雄记念碑
我今天去了天安门看人民英雄纪念碑
错误可能是同音字或形近字等
步骤:
① 对每一个字建立一个混淆字集合
② 计算整句话成句概率
③ 用混淆字集合中的词替代原句中的字,重新计算概率
④ 选取得分最高的一个候选句子,如果这个句子比原句的得分增长超过一定的阈值
⑤ 对下一个字重复步骤3-4,直到句子末尾
同音字有完整字表 形似字可以通过 ocr 收集 也可以依照实际情况手动添加一些
缺陷:
这种方式有一些缺陷:
① 无法解决多字少字问题
② 阈值的设置非常难把握,如果设置过大,达不到纠错效果;如果设置过小,造成大量替换,有可能改变句子的原意
③ 混淆字字表难以完备(错误可能是同音字或形近字)
④ 语言模型的领域性会影响修改结果
⑤ 连续的错字会大幅提升纠错难度,如:今天上午要过来办事 -> 今天商务要过来办事
一般工业做法:
限定一个修改白名单,只判断特定的字词是否要修改
如限定只对所有发音为shang wu的片段,计算是否修改为“商务”,其余一概不做处理
对于深度学习模型而言,错别字是可以容忍的,所以纠错本身的重要性在下降,一般只针对展示类任务
3.数字归一化
将一个文本中的数字部分转化为对读者友好的样式
常见于语言识别系统后,展示文本时使用
如:
秦皇岛港煤炭库存量在十一月初突然激增,从四百五十四点九万吨增加到七百七十三点四万吨,打破了一九九九年以来的记录
秦皇岛港煤炭库存量在11月初突然激增,从454.9万吨增加到773.4万吨,打破了1999年以来的记录
十一届三中全会、“十二五”规划、一贫如洗、二龙戏珠
步骤:
① 找到数字形式符合规范的文本作为原始语料
② 用正则表达式找到数字部分(任意形式)
③ 将数字部分依照其格式替换为<阿拉伯数字><汉字数字><汉字连读>等token
④ 使用带token文本训练语言模型
⑤ 对于新输入的文本,同样使用正则表达式找到数字部分,之后分别带入各个token,使用语言模型计算概率
⑥ 选取概率最高的token最为最终数字格式,按照规则转化后填入原文本
4.文本打标
给文本添加标点或语气停顿等
例:我最近 抽了点时间 读了一本 关于 马尔可夫生平 的书
可以理解为一种粗粒度的分词
常用于语音合成任务中,辅助做出发音的停顿
需要有标注数据,在停顿处添加token: <s>
如: 我最近 <s> 抽了点时间 <s> 读了一本 <s> 关于 <s> 马尔可夫生平 <s> 的书
带token训练语言模型
预测过程:
① 选定一个窗口长度,首先预测第一次停顿位置
我<s>最近抽了点时间 ppl:10
我最<s>近抽了点时间 ppl:20
我最近<s>抽了点时间 ppl:5
② 选择ppl值较小的地方作为第一次停顿
…、
③ 之后从“抽了点时间”开始向后重复此过程
本质为序列标注任务
可以依照类似方式,处理分词、文本加标点、文本段落切分等任务
分词或切分段落只需要一种token;打标点时,可以用多种分隔token,代表不同标点
十、总结
1.语言模型的核心能力是计算成句概率(计算下一个字的概率分布),依赖这一能力,可以完成大量不同类型的NLP任务。
2.基于统计的语言模型和基于神经网络的语言模型各有使用的场景,大体上讲,基于统计的模型优势在于解码速度,而神经网络的模型通常效果更好。
3.单纯通过PPL评价语言模型是有局限的,通过下游任务效果进行整体评价更好。
4.深入的理解一种算法,有助于发现更多的应用方式。
5.看似简单(甚至错误)的假设,也能带来有意义的结果,事实上,这是简化问题的常见方式。