news 2026/2/25 12:27:25

生成式深度学习(文本生成)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
生成式深度学习(文本生成)

文本生成

本节将介绍如何利用RNN 来生成序列数据。我们将以文本生成为例,但同样的技术也可以
推广到任意类型的序列数据,你既可以将其应用于音符序列来生成新音乐,也可以应用于笔画数
据时间序列(比如艺术家在iPad 上绘画时记录的笔画数据)来一笔一笔地生成绘画,诸如此类。

序列数据生成绝不仅限于艺术内容生成。它已经成功应用于语音合成和聊天机器人的对话
生成。谷歌在2016 年发布了Smart Reply(智能回复)功能,它能够对电子邮件或短信自动生
成一组快速回复,使用的也是类似的技术。

生成式深度学习用于序列生成的简史

在2014 年末,很少人知道LSTM 这一缩写词,即使在机器学习领域也是如此。用循环网络
生成序列数据的成功应用直到2016 年才开始出现在主流领域。但是,这些技术都有着相当长的历
史,最早的是1997 年开发的LSTM 算法(参见第10 章),这一算法早期用于逐个字符生成文本。

2002 年,当时在瑞士Schmidhuber 实验室工作的Douglas Eck 首次将LSTM 应用于音乐生
成,并得到了令人满意的结果。Eck 现在是谷歌大脑(Google Brain)的研究人员,2016 年他在
那里创建了一个名为Magenta 的新研究小组,重点研究将现代深度学习技术用于制作迷人的音
乐。有时,好的想法需要15 年才能变成现实。

在20 世纪末和21 世纪初,Alex Graves 在利用循环网络生成序列数据方面做了重要的开创
性工作,特别是他在2013 年的工作,利用笔尖位置的时间序列将循环混合密度网络用于生成类
似人类的手写笔迹,有人认为这是一个转折点a。神经网络的这个特定应用在那个特定时刻,用
能够做梦的机器这一概念适时地引起了我的兴趣,并且在我开始开发Keras 时为我提供了重要
的灵感。Graves 在2013 年上传到预印本服务器arXiv 上的LaTeX 文件中留下了一条类似的注释
性评论:“生成序列数据是计算机所做的最接近于做梦的事情。”数年过后,我们将这些进展视
为理所当然,但在当时,看到Graves 的演示,很难不为其中所蕴含的可能性所震撼。2015 年~
2017 年,RNN 已成功应用于文本和对话生成、音乐生成和语音合成。

然后在2017 年~ 2018 年,Transformer 架构开始取代RNN,它不仅可用于有监督的NLP
任务,还可用于生成式序列模型,特别是语言模型(单词级文本生成)。生成式Transformer 最
有名的例子应该是GPT-3,它是一个包含1750 亿个参数的文本生成模型,由初创公司OpenAI
在一个大得惊人的文本语料库上训练得到。该语料库包含大部分数字图书、维基百科和对整个
互联网进行爬取的大部分内容。GPT-3 在2020 年登上新闻头条,因为它能够在几乎所有话题上
生成看似合理的文本段落,这种能力引发了短暂的炒作热潮,堪比最火热的人工智能夏天。

如何生成序列数据

用深度学习生成序列数据的通用方法,就是利用前面的词元作为输入,训练模型(通常是
Transformer 或RNN)来预测序列中接下来的一个或多个词元。例如,给定输入“the cat is on
the”,训练模型来预测目标“mat”,即下一个单词。与前面处理文本数据一样,词元通常是单
词或字符。给定前面的词元,能够对下一个词元的概率进行建模的任何神经网络都叫作语言模
型(language model)。语言模型能够捕捉到语言的潜在空间,即语言的统计结构。

训练好这样的语言模型之后,你可以从中进行采样(sample,即生成新序列)。你可以向模
型输入一个初始文本字符串[叫作条件数据(conditioning data)],让模型生成下一个字符或下
一个单词(甚至可以一次性生成多个词元),然后将生成的输出添加到输入数据中,并多次重复
这一过程,如图12-1 所示。这个循环过程可以生成任意长度的序列,这些序列反映了模型训练
数据的结构,它们与人类写出的句子几乎相同。

采样策略的重要性

生成文本时,如何选择下一个词元是非常重要的。一种简单的方法是贪婪采样(greedy
sampling),就是始终选择可能性最大的下一个字符。但这种方法会得到重复、可预测的字符串,
看起来不像是连贯的语言。一种更有趣的方法是做出稍显意外的选择:在采样过程中引入随机
性,从下一个字符的概率分布中进行采样。这叫作随机采样(stochastic sampling,你应该还记得,
stochasticity 在这个领域是“随机”的意思)。在这种情况下,根据模型结果,如果下一个单词
是某个单词的概率为0.3,那么你会有30% 的概率选择它。请注意,贪婪采样也可看作从概率
分布中进行采样,即某个单词的概率为1,其他所有单词的概率都是0。

从模型的softmax 输出中进行概率采样是一种很巧妙的方法,它甚至可以在某些时候采样
到不常见的单词,从而生成看起来更加有趣的句子,而且有时会生成训练数据中没有、看起来
像是真实存在的新句子,从而展现创造性。但这种策略有一个问题:它在采样过程中无法控制
随机性的大小。

为什么要控制随机性的大小?考虑一种极端情况——纯随机采样,即从均匀概率分布中抽
取下一个单词,每个单词的概率相等。这种方法具有最大的随机性,换句话说,这种概率分布
具有最大的熵。自然,它不会生成任何有趣的内容。再来看另一种极端情况——贪婪采样,它
也不会生成任何有趣的内容。它也没有任何随机性,相应的概率分布具有最小的熵。从“真实”
概率分布(模型softmax 函数输出的分布)中进行采样,是这两种极端之间的一个中间点。但是,
还有许多其他中间点具有更大或更小的熵,你可能都想研究一番。更小的熵可以让生成序列具
有可预测性更强的结构(从而可能看起来更加真实),而更大的熵则会得到更加出人意料、更有
创造性的序列。从生成式模型中进行采样时,在生成过程中探索不同的随机性大小总是不错的
做法。我们人类最终来判断生成数据是否有趣,所以有趣是非常主观的,我们无法提前知道最
佳熵是多大。

为了控制采样过程中的随机性大小,我们引入一个叫作softmax 温度(softmax temperature)
的参数,表示用于采样的概率分布的熵,即所选择的下一个单词有多么出人意料或多么可预测。
给定一个temperature 值,我们将按照下列方法对原始概率分布(模型的softmax 输出)进行
重新加权,计算出一个新的概率分布,如代码清单12-1 所示。

代码清单12-1 对于不同的softmax 温度,对概率分布进行重新加权

更高的温度得到的是熵更大的采样分布,会生成更加出人意料、结构性更弱的数据;而更
低的温度则对应更小的随机性,以及可预测性更强的生成数据,如图12-2 所示。

用Keras 实现文本生成

下面我们用Keras 来实现上述想法。首先需要大量文本数据,你可以利用这些数据学习一
个语言模型。你可以使用任意足够大的一个或多个文本文件,如维基百科、《指环王》等。

本例将继续使用第11 章的IMDB 影评数据集,并学习生成前所未见的影评。也就是说,我
们的语言模型是针对这些影评的风格和主题的模型,而不是关于英语语言的通用模型。

1. 准备数据

我们已经在第11 章中下载了IMDB 影评数据集。现在将数据集解压,如代码清单12-2
所示。

代码清单12-2 将IMDB 影评数据集解压
!tar -xf aclImdb_v1.tar.gz
你已经熟悉了数据的结构:一个名为aclImdb 的文件夹,其中包含两个子文件夹,一个
保存负面情绪的影评,另一个保存正面情绪的影评。每条影评都是一个文本文件。我们调用
text_dataset_from_directory() 并设置label_mode=None,创建一个数据集,从这些文
件中读取并生成每个文件的文本内容,如代码清单12-3 所示。

代码清单12-3 利用文本文件创建数据集(一个文件即一个样本)

接下来,我们使用TextVectorization 层计算词表,如代码清单12-4 所示。我们只使用
每条评论中的前sequence_length 个单词,TextVectorization 层在对文本进行向量化时,
将截断超过这个长度的内容。

代码清单12-4 准备TextVectorization 层

我们使用该层来创建一个语言模型数据集,如代码清单12-5 所示,其中输入样本是向量化
文本,对应的目标是偏移了一个单词的相同文本。

代码清单12-5 创建语言模型数据集

2. 基于Transformer 的序列到序列模型

给定一些初始单词,我们将训练模型来预测句子下一个单词的概率分布。模型训练完成后,
我们首先给它一个提示词,对下一个单词进行采样,并将这个单词添加到提示词中,然后不断重复这一过程,直到生成一个简短的段落。

就像第10 章中的温度预测任务那样,我们可以训练一个模型,以N 个单词的序列作为输入,
并预测第N+1 个单词。然而,对于序列生成来说,这种方法有以下几个问题。

首先,模型只能在已有N 个单词时学习生成预测,但如果在少于N 个单词的情况下就能开
始预测,那会很有用。否则,我们将受限于只能使用相对较长的提示词(这里N=100)。第10
章并没有这种需求。

其次,许多训练序列在很大程度上是彼此重合的。比如对于N=4,使用文本“A complete
sentence must have, at minimum, three things: a subject, verb, and an object”(一个完整的句子必须
至少有三部分:主语、动词和宾语)可以生成以下训练序列:
-“ A complete sentence must”
-“ complete sentence must have”
-“ sentence must have at”

  • 一直到“verb and an object”

为了解决上述两个问题,我们将使用序列到序列模型:将N 个单词的序列(索引从0 到N)
输入模型,并预测偏移1 个单词后的序列(索引从1 到N+1)。我们将使用因果掩码,以确保对
于任意i,模型都只使用第0 ~ i 个单词来预测第i+1 个单词。这意味着我们训练模型来同时
解决N 个问题,这N 个问题在很大程度上彼此重叠但又各不相同:给定前面索引1<=i<=N 的
单词序列,预测下一个单词,如图12-3 所示。在生成阶段,即使只给模型一个提示词,它也能
够给出后续单词的概率分布。

请注意,我们在第10 章的温度预测问题上也可以使用类似的序列到序列方法:给定120 个
数据点的序列(每小时1 个数据点),学习生成120 个温度的序列,均向后偏移24 小时。这样
一来,你不仅解决了最初的问题,还解决了119 个相关问题,即给定前面1<=i<120 个每小时数据点,预测24 小时后的温度。如果你尝试用序列到序列方法重新训练第10 章的RNN,你会
得到类似但稍差的结果,因为用同一个模型要额外解决119 个相关问题,这项约束会稍微影响
我们真正关心的任务。

第11 章介绍过通用的序列到序列学习:将源序列输入编码器,然后将编码后的序列与目标
序列输入解码器,解码器会尝试预测偏移一个时间步的相同目标序列。进行文本生成时,没有
源序列,你只是在给定前面词元的情况下尝试预测目标序列的下一个词元,可以只使用解码器
来完成。由于使用了因果填充,解码器将仅通过查看第0 ~ N 个单词来预测第N+1 个单词。

我们来实现模型,如代码清单12-6 所示。我们将复用第11 章创建的组件:Positional-
Embedding 和TransformerDecoder。

代码清单12-6 基于Transformer 的简单语言模型

带有可变温度采样的文本生成回调函数

利用回调函数,我们可以在每轮之后使用一系列不同的温度来生成文本,如代码清单12-7
所示。你可以看到随着模型开始收敛,生成文本如何演变,还可以看到温度对采样策略的影响。
我们将使用提示词“this movie”(这部电影)作为文本生成的种子,也就是说,所有生成文本都
将以此开头。

代码清单12-7 文本生成回调函数


下面来拟合这个模型,如代码清单12-8 所示。
代码清单12-8 拟合语言模型

model.fit(lm_dataset,epochs=200,callbacks=[text_gen_callback])

训练200 轮之后生成的一些精选示例如下所示。请注意,因为词表中不包含标点,所以我
们生成的文本中也没有任何标点。
请注意,利用更多的数据训练一个更大的模型,并且训练时间更长,生成的样本会比上面
的结果看起来更加连贯、更加真实——GPT-3 等模型的输出就是很好的例子,可以展示语言模
型能够做什么(GPT-3 实际上和本例中训练的模型是一样的,只不过堆叠了多个Transformer 解
码器,而且还有更大的训练语料库)。但是,不要期待模型能够生成任何有意义的文本,除非是
很偶然的情况并且你加上了自己的解释。你所做的只是从一个统计模型中采样数据,这个模型
表示的是哪些单词出现在哪些单词之后。语言模型只有形式,没有实质内容。

自然语言有很多用途:它是一种交流渠道,一种对世界采取行动的方式,一种社交润滑剂,
一种表达、存储和检索自己思想的方式……语言的这些用途是其意义的来源。一个深度学习“语
言模型”,尽管叫这个名字,但实际上并不具有语言的这些基本特征。它不能交流(没有什么可
交流的内容,也没有人可以交流),不能对世界采取行动(没有行为体,也没有意图),不能社交,
也没有任何思想要用语言来表达。语言是大脑的操作系统。因此,要让语言变得有意义,需要
有大脑来使用它。

语言模型的作用是捕捉我们在生活中使用语言时生成的可观察人工制品(如书籍、在线影
评、推文)的统计结构。这些人工制品具有统计结构这一事实,是人类语言实现方式的副作用。
我们来看一个思想实验:如果人类语言能够更好地压缩通信,就像大多数计算机数字通信所做
的那样,那么会发生什么?语言的意义不会减小,仍然可以实现它的诸多目的,但不会具有任
何内在的统计结构,所以不可能像刚才那样构建一个语言模型。
完整代码

import tensorflow as tf import numpy as np import os from tensorflow import keras from tensorflow.keras import layers # 1. 准备数据 # 假设IMDB数据集已解压到当前目录 # !tar -xf aclImdb_v1.tar.gz # 创建数据集 batch_size = 32 sequence_length = 100 text_ds = tf.keras.utils.text_dataset_from_directory( "aclImdb", label_mode=None, batch_size=batch_size ) # 准备TextVectorization层 vocab_size = 15000 text_vectorization = layers.TextVectorization( max_tokens=vocab_size, output_mode="int", output_sequence_length=sequence_length + 1 # +1用于创建目标序列 ) # 只使用训练数据来适配向量化器 train_text_ds = text_ds.map(lambda x: x) text_vectorization.adapt(train_text_ds) # 创建语言模型数据集 def prepare_lm_dataset(text_batch): vectorized_batch = text_vectorization(text_batch) # 输入是前N个单词,目标是偏移一个单词的序列 x = vectorized_batch[:, :-1] y = vectorized_batch[:, 1:] return x, y lm_dataset = text_ds.map(prepare_lm_dataset).prefetch(buffer_size=tf.data.AUTOTUNE) # 2. 定义Transformer组件 class PositionalEmbedding(layers.Layer): def __init__(self, sequence_length, vocab_size, embed_dim, **kwargs): super().__init__(**kwargs) self.token_embeddings = layers.Embedding( input_dim=vocab_size, output_dim=embed_dim ) self.position_embeddings = layers.Embedding( input_dim=sequence_length, output_dim=embed_dim ) self.sequence_length = sequence_length self.vocab_size = vocab_size self.embed_dim = embed_dim def call(self, inputs): length = tf.shape(inputs)[-1] positions = tf.range(start=0, limit=length, delta=1) embedded_tokens = self.token_embeddings(inputs) embedded_positions = self.position_embeddings(positions) return embedded_tokens + embedded_positions def compute_mask(self, inputs, mask=None): # 使用keras.ops.not_equal替代tf.math.not_equal return keras.ops.not_equal(inputs, 0) class TransformerDecoder(layers.Layer): def __init__(self, embed_dim, dense_dim, num_heads, **kwargs): super().__init__(**kwargs) self.embed_dim = embed_dim self.dense_dim = dense_dim self.num_heads = num_heads self.attention = layers.MultiHeadAttention( num_heads=num_heads, key_dim=embed_dim ) self.dense_proj = keras.Sequential([ layers.Dense(dense_dim, activation="relu"), layers.Dense(embed_dim), ]) self.layernorm_1 = layers.LayerNormalization() self.layernorm_2 = layers.LayerNormalization() self.supports_masking = True def call(self, inputs, mask=None): causal_mask = self.get_causal_attention_mask(inputs) if mask is not None: padding_mask = tf.cast( mask[:, tf.newaxis, :], dtype="int32" ) padding_mask = tf.minimum(padding_mask, causal_mask) attention_output = self.attention( query=inputs, value=inputs, key=inputs, attention_mask=causal_mask ) proj_input = self.layernorm_1(inputs + attention_output) proj_output = self.dense_proj(proj_input) return self.layernorm_2(proj_input + proj_output) def get_causal_attention_mask(self, inputs): input_shape = tf.shape(inputs) batch_size, sequence_length = input_shape[0], input_shape[1] i = tf.range(sequence_length)[:, tf.newaxis] j = tf.range(sequence_length) mask = tf.cast(i >= j, dtype="int32") mask = tf.reshape(mask, (1, input_shape[1], input_shape[1])) mult = tf.concat( [tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)], axis=0 ) return tf.tile(mask, mult) # 3. 定义基于Transformer的语言模型 embed_dim = 256 dense_dim = 2048 num_heads = 8 inputs = layers.Input(shape=(None,), dtype="int64") x = PositionalEmbedding(sequence_length, vocab_size, embed_dim)(inputs) x = TransformerDecoder(embed_dim, dense_dim, num_heads)(x) x = TransformerDecoder(embed_dim, dense_dim, num_heads)(x) outputs = layers.Dense(vocab_size, activation="softmax")(x) model = keras.Model(inputs=inputs, outputs=outputs) # 编译模型 model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) # 4. 文本生成回调函数 class TextGenerator(keras.callbacks.Callback): def __init__(self, prompt, text_vectorization, temperatures=[0.2, 0.5, 1.0, 1.5], print_every=1): super().__init__() self.prompt = prompt self.text_vectorization = text_vectorization self.temperatures = temperatures self.print_every = print_every self.vocab = text_vectorization.get_vocabulary() self.token_index_lookup = dict(zip(self.vocab, range(len(self.vocab)))) def on_epoch_end(self, epoch, logs=None): if (epoch + 1) % self.print_every != 0: return print(f"\n=== Epoch {epoch + 1} ===") for temperature in self.temperatures: print(f"\nTemperature: {temperature}") generated_text = self.generate_text(temperature, num_generated_tokens=30) print(generated_text) def sample_next_word(self, predictions, temperature=1.0): predictions = np.asarray(predictions).astype("float64") if temperature > 0: predictions = np.log(predictions + 1e-7) / temperature exp_preds = np.exp(predictions) predictions = exp_preds / np.sum(exp_preds) probs = np.random.multinomial(1, predictions, 1) return np.argmax(probs) def generate_text(self, temperature=1.0, num_generated_tokens=30): # 将提示词转换为token prompt_tokens = self.text_vectorization([self.prompt])[0].numpy() generated_tokens = list(prompt_tokens) for i in range(num_generated_tokens): # 获取最后一个sequence_length个token input_tokens = generated_tokens[-sequence_length:] # 预测下一个token predictions = self.model.predict( np.array([input_tokens]), verbose=0 )[0, -1, :] # 采样下一个token next_token = self.sample_next_word(predictions, temperature) generated_tokens.append(next_token) # 将token转换回文本 generated_text = "" for token in generated_tokens: if token == 0: # 填充token continue word = self.vocab[token] if word.startswith("[UNK]"): word = "?" generated_text += word + " " return generated_text.strip() # 5. 创建并训练模型 text_gen_callback = TextGenerator( prompt="this movie", text_vectorization=text_vectorization, temperatures=[0.2, 0.5, 1.0, 1.5], print_every=10 ) # 训练模型 print("开始训练模型...") history = model.fit( lm_dataset, epochs=50, # 先训练50轮看看效果 callbacks=[text_gen_callback] ) # 6. 模型摘要和保存 print("\n模型结构摘要:") model.summary() # 保存模型 model.save("text_generation_model.h5") print("模型已保存为 text_generation_model.h5") # 7. 使用训练好的模型生成文本 def generate_text_with_model(prompt, temperature=1.0, num_words=50): """使用训练好的模型生成文本""" text_gen = TextGenerator( prompt=prompt, text_vectorization=text_vectorization, temperatures=[temperature], print_every=100 # 设置为大值以避免自动打印 ) text_gen.model = model generated_text = text_gen.generate_text( temperature=temperature, num_generated_tokens=num_words ) return generated_text # 示例:使用不同温度生成文本 print("\n" + "=" * 60) print("训练完成,测试不同提示词的生成效果:") print("=" * 60) prompts = [ "this movie is", "the best film", "i really liked", "the worst movie" ] for prompt in prompts: print(f"\n提示词: '{prompt}'") print("-" * 50) for temp in [0.2, 0.5, 1.0, 1.5]: result = generate_text_with_model(prompt, temperature=temp, num_words=30) print(f"温度 {temp}: {result}") print()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/20 2:38:34

树莓派CAN(FD) 测试RS232 RS485 CAN Board 测试

RS232 RS485 CAN Board 测试#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Enhanced UART CAN loopback test (Python3) - 彩色输出 - 每次发送 HelloLoop-8888&#xff08;固定数字&#xff09; - 丢包率/数据长度统计 - 包含 ttySC0, ttySC1, ttyS0 和…

作者头像 李华
网站建设 2026/2/21 3:20:48

R量子计算入门到精通(门操作序列优化全解析)

第一章&#xff1a;R量子计算与门操作序列基础量子计算利用量子比特&#xff08;qubit&#xff09;的叠加与纠缠特性&#xff0c;实现远超经典计算的并行处理能力。在R语言中&#xff0c;虽然并非主流的量子编程平台&#xff0c;但借助特定模拟库如 quantum 或 qsimulatR&#…

作者头像 李华
网站建设 2026/2/23 10:02:55

Highcharts Dashboards 之明和暗主题设置使用文档

亮色与暗色自适应主题允许你在仪表盘的亮色主题和暗色主题之间切换。 要使用亮色与暗色主题&#xff0c;你需要导入 dashboards.css 文件。 import "https://code.highcharts.com/dashboards/css/dashboards.css";接下来&#xff0c;如果你的仪表盘包含带有Highchar…

作者头像 李华
网站建设 2026/2/24 19:19:57

底层通信架构GRPC

通过protobuf编码、基于Netty 去传输1、客户端java、服务端用的是GO&#xff0c;那么都可以通过GRPC远程调用。proto文件作为标准。2、底层是用netty协议&#xff0c;netty底层是一种长连接&#xff0c;性能高。netty底层是基于nio&#xff0c;非阻塞。3、编码格式是protobuf&a…

作者头像 李华
网站建设 2026/2/21 19:40:47

揭秘农业物联网数据瓶颈:如何用PHP优化传感器数据存储性能

第一章&#xff1a;农业物联网与PHP技术融合的背景随着现代农业向智能化、精细化方向发展&#xff0c;农业物联网&#xff08;Agri-IoT&#xff09;正逐步成为提升农业生产效率的核心驱动力。通过传感器、无线通信和数据处理技术&#xff0c;农业物联网实现了对土壤湿度、环境温…

作者头像 李华