AIGC
叫“生成式人工智能”。想必“人工智能”大家都很熟悉了,因此我想跟大伙儿聊聊这个“生成式”。
AIGC
中的GC
不是指“国粹”,是Generated Content
的缩写,表示生成内容。看下面这段文本:
掘井须到头 金刀剪紫绒 社日放歌还 区区趁适来
掘井须到南 金钿坠芳草 社会前年别 区区趁试期
掘井须到三 金钏色已歇 社客加笾食 区区趁试肠
掘井似翻罗 金钗已十年 社日穿痕月 区区趁试侣
怎么样?是不是有种《诗经》中“回旋往复,一唱三叹”的韵味。
其实,这每一行都是藏头诗。你仔细看看,第一个字连起来就是:掘金社区
。
上面的内容,是我用AI
生成的。
下面的内容,我将从原理到操作,讲一下如何不调API
,自己动手写这么个同款AIGC
。
文本序列化
我们先来做一个小游戏。往往小游戏里蕴含着大道理。
看这么一组数据:
序列1 | 序列2 | 序列3 | 序列4 | 序列5 | 序列6 |
---|---|---|---|---|---|
1 | 2 | 4 | 8 | 16 | ? |
请问,序列6
的值是多少?
答案是32
!解题思路:后一个数的值是前一个数的2倍。
我还可以搞更多的数据组,让你去探索。比如:
- 后一个数是前面所有数之和。
- 后一个数是前面2个数的3倍减去22。
即便实在是找不到规律了。比如就是一串毫无规律的乱码。请问它的后面该是什么?将这串乱码再重复循环,也算是规律。
好了。到这里我想给大家引入一个概念叫做序列
。
文字领域它有一个特点,那就是具有序列(sequence)特征。
我每天都访问掘金社区,从这里我能获取很多关于___的知识。
空白处该填什么呢?填“备孕”肯定不合适。你可以填“前端”、“人工智能”、“互联网技术”。
哎!你为什么会这么想?因为“掘金社区”是一个IT
开发者社区。这句话前边的前提,会影响后边的判断,这就是序列的特点。
那位吃冰糕的先生说了,数字序列的规律我能看出来,但是文字的规律怎么看呢?
其实,我们可以把文字转为数字。
序列1 | 序列2 | 序列3 | 序列4 | 序列5 | |
---|---|---|---|---|---|
文字 | 春 | 眠 | 不 | 觉 | 晓 |
数字 | 0 | 1 | 2 | 3 | 4 |
这样转换后,春晓
可用04
表示,它俩是等价的。
既然我们可以分辨出优雅的诗句与普通的俗语,这说明它是有特征和规律的。
因此,我们可以研究这些序列化后的文本,找找它们前后之间的联系和规律。
那么问题来了,有没有一个好的方案去解决这个问题呢?
循环神经网络
小弟我上一篇讲了CNN
卷积神经网络,它主要解决图像领域的问题。但是,到了文字领域,CNN
就怂了。因为它全靠凑数,没有时间序列这条线。
为了处理具有顺序特征的数据,就出现了循环神经网络
(Recurrent Neural Network)也就是RNN
。RNN的重点在R(Recurrent 循环)上。它可以将上一个时间步骤的状态,作为当前时间步骤的输入,从而捕捉到序列中的时间依赖关系,并利用先前的状态来影响当前状态的更新。
我的话让你感觉很抽象,你也很想抽我。我发一张图给大家缓解一下。
RNN
的结构图一般是这样的:
这张图解释了RNN
是如何循环的。X
是输入,A
是神经单元,h
是输出。输入、输出都带着时间t
。这个小单元,他就左手倒右手,转着圈玩儿。凡是经他手的数据,它都会记下一些特征,然后共享这些数据。因此,它就有了记忆。
今天大家来到我的直播间……呸,来到我的掘金专栏,我给大家来点更生动的。
我的读者多是初学者。因此,我们暂且认为
RNN
、LSTM
、GRU
都一样(就像包子、馅饼都是面食)。我不去讲它们的演变史(我讨厌技术课变为历史课)。
下面这幅图是一个RNN
的变种(我就不说是LSTM)。我们不用关心内部结构,主要看它的数据流转。在某个序列t
时,将数据X
输入。这个输入数据的格式,后边会重点讲。在这里,大家要了解数据是依次导入的,也就是一条数据转一个单元。
再看下图,这里演示了细节。X·t
是和h·t-1
一起进入的。如果X·t
这个时刻代表“春眠不觉晓”的“眠”,那么h·t-1
里肯定也包含了“春”的记忆信息。
上面是序列在t
时刻进入神经单元的情况。在里面经历一番数据操作(保留、遗忘)后,它继续往下传递。下面是它的输出示意图。他结合了h·t-1
并加上自己的记忆,又输出了新的h·t
供后面使用。
RNN就是这样运作的。循环、递归、记忆传递是它的主题。
好了。道理就讲到这里,再多说,你就要关页面了。下面开干,写代码!
实践项目:AI藏头诗
-
项目名称:利用
RNN
实现文本生成,写藏头诗。 -
运行环境:
Python 3.9
、TensorFlow 2.6
。 -
代码来源:
ChatGPT
。
既然本文讲AIGC
,那么我们就要体现出它的妙用。我向ChatGPT咨询如何实现我的想法,它给出了答案。
作为专业演员……人员。我负责地告诉你,它的回答,步骤明确、完全无误,在数据集完备的情况下,可以直接运行。
这说明了什么?
- 第一,人工智能的初级应用并不遥远,也并非被束之高阁无法企及。
- 第二,简单的需求真的是马路边上的东西,但是不为人所知,因为没有人去传播。
下面,我就拿老Chat的代码,按照我的思路,逐一讲解并实现。
搭建神经网络
刚刚学了RNN,那么我们就来组建一套循环神经网络。tensorflow
可以通过Sequential
来构建网络模型。
import tensorflow as tf
vocab_size = len(vocab)# 词汇表大小
embedding_dim = 256 # 嵌入维度
rnn_units = 1024 # RNN单元数量
batch_size = 32 # 批次大小
# 构建一个模型
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
batch_input_shape=[batch_size, None]),
tf.keras.layers.GRU(rnn_units,
return_sequences=True,
stateful=True,
recurrent_initializer='glorot_uniform'),
tf.keras.layers.Dense(vocab_size)
])
上面这段代码是整个例子的核心部件。其上游代码为它提供弹药,下游的代码则分析它的产出。
这个结构很简单,Sequential
中就三个层:Embedding
作为数据输入、GRU
作为神经单元处理、Dense
作为结果输出。
下图拿生成诗句举例,对每层做剖析。
中间是我们熟悉的RNN
层,虽然它叫GRU
,但它属于RNN
家族。代码中参数rnn_units
是神经单元的数量,这个我们可以自己调。我们上面说那么热闹,其实只说了一个基本单元的运转。到真正干活时,是需要一堆单元来处理一条数据的(上图X
要连接多个RNN
单元)。其他的参数你记住就可以了。
下面说说词汇表和嵌入表。不管输入还是输出,我们都要用到它。正是通过它们才将文本转为数字的。
你想让RNN自动生成,前提是它得有料。词汇库就是这个料。
假设我们拿一堆诗句来训练模型,现在诗句也已经变为了数字。接着,我们要做两件事情:
- 第一:将数据传入模型,让它找规律(联想1、2、4、8那个游戏,规律是2倍关系)。
- 第二:给定一个起始值(藏头诗也是给一个字),让模型预测后面的数字(32后面是64,64后面是128)。
为了实现上述内容。我们定义了词汇表,用于文本转数字(眠->1)。
我们又定义了嵌入表,让它对一个字进行多维度的描述。0、1、2作为字的代号还可以。春眠是01
,春晓是04
。但对于文本生成来说,它的颗粒度太糙了。凭这,想要找出文学中深层次的内在规律,根本不可能。还是得将这个字拆成粉末,转为几百维度的向量靠谱。
每个字的向量具体是多少,你肯定不知道。我们正是想让神经网络自己去找(反向传播算法)。于是,我们只下定义就行。因此,我们看到代码中出现了embedding_dim = 256
,这是一个有256个维度的嵌入向量。
我看看还有啥值得说的。
哦!Dense(vocab_size)
表示输出层的大小是词汇表的大小。意思就是说,它推测出来的下一个字,是词汇表中的某一个字。
读到这里,你再回头看看上面的图,基本上就恍然大悟了。
好了,大脑已经完成了,下面就让我们去写无脑的代码吧。
处理训练数据集
既然想要生成藏头诗,那么首先得有一些诗句作为引子。
我自己攒了一些五言古诗+五言对联的句子。有多好不敢说,反正文绉绉的,大约15万条。
首先读入这个文件,并生成词汇表。
# 读取文件
with open('poetry.txt', 'r', encoding='utf-8') as file:
text = file.read()
# 去除重复字符并排序
vocab = sorted(set(text))
# 创建字符到索引的映射字典
char2idx = {char: idx for idx, char in enumerate(vocab)}
poetry.txt
就是存放五言句子的文件。这个文件连同源码,我会上传到Github
,链接会附在文末。
建议大家使用
jupyter
工具,可以逐步执行,查看执行情况。
上面的代码,text
读取了文本内容,vocab
是文中所有出现过的字,做了去重和排序处理。这就是我们的词汇库。有了它,文字就可以变为数字了。
text = "春眠不觉晓 处处闻啼鸟 夜来风雨声 花落知多少 ……"
vocab = [' ', '一', '丁', '七', '万', '丈', '三', ……]
下一步,我们开始组织训练用的数据集。
# 将文本切分为5字一组的数组
sentences = text.split(" ")
x_sequences = []
y_sequences = []
# 组织出训练集(输入+输出)
for sentence in sentences:
x = [char2idx[char] for char in sentence[:-1]]
y = [char2idx[char] for char in sentence[1:]]
x_sequences.append(x)
y_sequences.append(y)
# 转为tensor数据
datasets = tf.data.Dataset.from_tensor_slices((x_sequences, y_sequences))
# 将数据批次化
datasets = datasets.batch(batch_size, drop_remainder=True)
上面多是常规操作,我着重讲一下输入和输出的制作。
我们期望模型能达到较高的认知水平。举个例子,我们起一个头叫“春”,模型会生成下一个字是“眠”。以此类推,训练集的输入和输出总是会前后错开一个字。
这就是为什么输入sentence[:-1]
不要最后一个字,输出取sentence[1:]
不要第一个字。我们期望模型按照这样的思路去探索规律。
训练模型,保存权重
训练的代码其实很简单:
# 定义损失函数
def loss(labels, logits):
return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)
model.compile(optimizer='adam', loss=loss)
# 训练回调,模型保存路径
callback=tf.keras.callbacks.ModelCheckpoint(filepath="tf2/checkpoint", save_weights_only=True)
# 进行训练
model.fit(datasets, epochs=100, callbacks=[callback])
只要对模型做简单的配置,然后调用model.fit
就可以训练了。
model
是我们一开始通过Sequential
创建的。datasets
是上一步组的数据集。epochs=100
是训练轮次。1个epochs表示把所有数据跑一遍。- 训练的最终模型将会保存为
tf2/checkpoint
。
这15万数据,对于普通电脑来说,压力很大。我设置的100epochs
才跑了40,就花费了3个多小时。模型的效果,就是文章开头的那样。
其实,大家在学习的时候,可以适当地减少数据量或者epochs
来提高验证效率。
训练过程会有如下打印:
Epoch 1/100
4600/4600 [====================] - loss: 5.8752
Epoch 2/100
4600/4600 [====================] - loss: 5.4711
每个Epoch
结束,都会有权重保存下来。
验证模型,生成藏头诗
正常情况下,训练完了。你只要给模型起个头,后面它就会滔滔不绝地输出。
# 给定1个字,变成5个字的方法
def generate_text(model, start_string):
num_generate = 4 # 生成的字符数
# 起始字序列化变为数字
input_eval = [char2idx[s] for s in start_string]
input_eval = tf.expand_dims(input_eval, 0)
text_generated = [] # 存放生成的字符
model.reset_states() # 清空本轮记忆
# 循环4次,每次生成一个字
for i in range(num_generate):
predictions = model(input_eval)
predictions = tf.squeeze(predictions, 0)
# 预测最可信答案的序号
predicted_id = tf.random.categorical(predictions, num_samples=1)[-1, 0].numpy()
# 把预测到的字作为输入,继续往下传递
input_eval = tf.expand_dims([predicted_id], 0)
# 根据编号找到字符,存起来
text_generated.append(idx2char[predicted_id])
return (start_string + ''.join(text_generated))
代码注释比较全面了。
start_string
是起始字符。就是藏头诗的第一个字。model
依然是我们建立的那个RNN序列模型。如果要加载训练好权重,可以调用model.load_weights("tf2/checkpoint")
。
我们实验一下效果:
generate_text(model, "请")
请以端溪润
generate_text(model, "嫁")
嫁来贤妻喜
generate_text(model, "给")
给园支遁隐
generate_text(model, "我")
我欲掣曳箭
咋说呢?我们不是专业的诗人。但是,单看这个词汇与风格,唬人应该不成问题。
说正经的,这用的是古诗数据,它会生成古诗。如果提供别的呢?
我希望本文能给诸位起一个头,让它四处散播,然后在各个行业与领域,生根发芽。
整体项目源码地址(含数据集):github.com/hlwgy/jueji… 。
本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!