【零基础成为 AI 解梦大师秘笈】Day29 - 周易解梦之人工智慧(10)

LSTM

前言

系列文章简介

大家好,我们是 AI . FREE Team - 人工智慧自由团队,这一次的铁人赛,自由团队将从0到1 手把手教各位读者学会 (1)Python基础语法 (2)Python Web 网页开发框架 – Django (3)Python网页爬虫 – 周易解梦网 (4)Tensorflow AI语言模型基础与训练 – LSTM (5)实际部属AI解梦模型到Web框架上。

为什麽技术要从零开始写起

自由团队的成立宗旨为开发AI/新科技的学习资源,提供各领域的学习者能够跨域学习资料科学,并透过自主学习发展协杠职涯,结合智能应用到各式领域,无论是文、法、商、管、医领域的朋友,都可以自由的学习AI技术。

资源

AI . FREE Team 读者专属福利 → Python Basics 免费学习资源

接下来介绍LSTM的部分,继上次介绍完RNN(Recurrent Neural Network),这次我们就来看一下LSTM的起源与原理

起源

RNN再长篇文章的发挥中,经常会有前期资讯到後面影响的决策影响越小,也就是前面的权重再後期的影响会越来越小的梯度消失问题

而为了解决这个问题,1997年 LSTM(Long Short-Trem Memory) 首次发表於论文上,其独特的设计可以良好的处理间隔与时序较长的Task,而真正发扬光大是在2009年的ICDAR的手写识别比赛,透过LSTM建立的模型取得冠军。

架构与公式

LSTM虽然看起来很复杂,但是在计算上其实与RNN是有相似之处,都是会透过前一个的输出加上这次计算的输入,不过LSTM特别之处就在有额外3个门闸(gate),会决定input是否重要到能被记住及能不能被输出output。

1.忘记阶段 :

  • 这个阶段主要是对上一个节点传进来的输入进行选择性忘记。简单来说就是会“忘记不重要的,记住重要的”。

2.选择记忆阶段 :

3.输出阶段 :

4.最後

利用LSTM进行解梦模型

  • 准备数据、前处理

※ 没有好的数据,演算法也做不出好的决策。

为什麽Google可以发展出这麽多好的预训练模型,还有让大家广为使用的youtube、gmail等等...

那是因为全世界每天有15亿的人,在帮 google 做决策(整理、准备数据)。

因此,这次的数据,我们就从灵狐算命网爬取资料

有了数据之後 接下来我们要将内容合并,并将空白行去掉
text_sentence = []
with open("clean_all.txt","r") as f:
  for i in f.readlines():
    if i.strip() != '':
      text_sentence.append(i.strip())

text =  ''.join(text_sentence)
text[:50]

这里的 with open("clean_all.txt","r") as f: 路径因人而异
大家放档案的位置可能都不同,我这里使用的是相对路径

资料集的字数多寡
n = len(text)
w = len(set(text))
print(f'这个资料集共有{n}个字')
print(f'这个资料集不重复的字有{w}个字')    
  • 将文字转成数字

当资料准备好也处理完之後,我们要将文本转成数字,因为电脑看的是数字不是文字,这个步骤大家应该都能够理解吧~

我们使用tensorflow.keras提供的tokenizer,来tokenize我们的文本,将文本中的每一个文字赋予一个唯一的数值,进而建立起一个字典

Minion

当然实际上不会是这样排的,要看python的记忆体位址怎麽给予~
这里我们import两个套件,作为python的使用者numpy大家肯定最熟悉不过了,他提供了很好的矩阵功能API,不论是在运算或是高阶函式库,都是大家做深度学习时最好的朋友~
另一个则是tensorflow,tensorflow是google实验室开发的深度学习框架,提供的高阶API可以加速我们完成这个任务
这里,我们宣告一个tf.keras.preprocessing.text.Tokenizer,来当作tokenizer
import tensorflow as tf
import numpy as np

# 初始化一个以字为单位的 Tokenizer
tokenizer = tf.keras.preprocessing.text.Tokenizer(
        num_words=w,
        char_level=True,
        filters=''
)
将我们的整份文本丢进去,它会将整份文本看过,第二行code则是将每个字赋值(int)
# 将文字转成int
tokenizer.fit_on_texts(text)
text_as_int = tokenizer.texts_to_sequences([text])[0]
print(text_as_int[:10])
len(text_as_int)
这里,我们简单看一下我们的文本与字典之间的关联,并使用tokenizer的function index_word 将数值转回文字,并检查
start_idx = 10
end_idx = 20
partial_indices = text_as_int[start_idx:end_idx]
partial_texts = [
    tokenizer.index_word[idx] for idx in partial_indices
]
print("原本的中文字序列:")
print()
print(partial_texts)
print()
print()
print("转换後的索引序列:")
print()
print(partial_indices)
现在整份解梦文本,都已经被转换成数值,就像摩斯密码一样

准备可以丢进模型的文本

看到这里你一定会有疑问,我该清理都清了,该转数值也转了,为什麽还不能丢进模型?

因为我们要先定义好,要丢给模型的资料集要长什麽样子,再对数据做适当的转换

AI 可以帮我们解决问题,但是 AI 不会知道问题是什麽,问题由我们定义!

再来回顾一下目前的资料集
_type = type(text_as_int)
n = len(text_as_int)
print(f"text_as_int 是一个 {_type}\n")
print(f"文本的序列长度: {n}\n")
print("前 5 索引:", text_as_int[:5])
给定一个字符或者一个字符序列,下一个最可能出现的字符是什麽?这就是我们训练模型要执行的任务。输入进模型的是一个字符序列,我们训练这个模型来预测输出 -- 每个步骤(time step)预测下一个字符是什麽。
print("实际丢给模型的数字序列:")
print(partial_indices[:-1])
print()
print("方便我们理解的文本序列:")
print(partial_texts[:-1])
而模型要给我们的理想输出应该是向左位移一个字的结果

print("实际丢给模型的文本序列:")
print(partial_texts[:-1])
print()
print("模型预期输出的文本序列:")
print(partial_texts[1:])
定义好问题之後,我们要使用 tensorflow 的API tf.data.Dataset.from_tensor_slices,将转换成数值的文本,转成tensorflow可以接受的tensor
# 将list转换成tensor
characters = tf.data.Dataset.from_tensor_slices(text_as_int)
characters
这里我们设定dataset里的每笔资料长度,使用 batch 这个内建的function
# tensorflow的dataset
SEQ_LENGTH = 10  # 数字序列长度
sequences = characters.batch(SEQ_LENGTH+1,drop_remainder=True)
for item in sequences.take(1):
  d = tokenizer.index_word
  print('dataset索引序列',[i.numpy() for i in item])
  print('dataset文本序列',[d[i.numpy()] for i in item])
宣告一个function build_seq_pairs 来帮我们建立成对的句子(输入/输出),这也是为什麽刚刚要 SEQ_LENGTH+1 ,因为我们丢入一个长度11的句子,他才能够返回长度各为10的(输入/输出),并使用dataset的内建function map ,让全部的文本都做过一轮
def build_seq_pairs(chunk):
    input_text = chunk[:-1]
    target_text = chunk[1:]
    return input_text, target_text
ds = sequences.map(build_seq_pairs)

for idx,item in enumerate(ds.take(2)):
  d = tokenizer.index_word
  print(f"第 {idx} 个输入句子的索引序列")
  print([i for i in item[0].numpy()])
  print(f"第 {idx} 个输入句子的文本序列:")
  print([d[i] for i in item[0].numpy()])
  print()
  print(f"第 {idx} 个输出句子的索引序列:")
  print([i for i in item[1].numpy()])
  print(f"第 {idx} 个输出句子的文本序列:")
  print([d[i] for i in item[1].numpy()])
  print('-'*100)
接下来我们要设定训练时,批次量BATCH_SIZE 以及设定缓冲区大小 BUFFER_SIZE 重新排列数据集

BATCH_SIZE 决定的是模型一次可以看多少个的句子,并使用GPU平行运算(效率)
这也是为什麽我们要使用 tensor

tensorflow被设定为可以处理无限的序列,它并不会在内存尝试不同的组合,因此需要宣告一个
缓冲区也就是BUFFER_SIZE,它维持一个缓冲区,在缓冲区重新排列元素

BATCH_SIZE = 64

BUFFER_SIZE = 10000 
ds = ds.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)

定义模型与超参数

这里我们用 keras 这个来自 tensorflow 的高级API来做实作
  • 使用 tf.keras.Sequential 定义模型。在这个简单的例子中,我们使用了三个层来定义模型:
    • tf.keras.layers.Embedding:输入层。一个可训练的对照表,它会将每个字符的数字映射到一个 embedding_dim 维度的向量。
    • tf.keras.layers.GRU:一种 RNN 类型,其大小由 units=rnn_units 指定(这里你也可以使用一个 LSTM 层)。
    • tf.keras.layers.Dense:输出层,带有 vocab_size 个输出。
EMBEDDING_DIM = 512
UNITS = 1024
LEARNING_RATE = 0.001

model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(input_dim=w, output_dim=EMBEDDING_DIM,batch_input_shape=[BATCH_SIZE, None]))
model.add(tf.keras.layers.LSTM(units=RNN_UNITS, return_sequences=True, stateful=True, recurrent_initializer='glorot_uniform'))
model.add(tf.keras.layers.Dense(w))

model.summary()
model.summary() 可以检视已经建完的模型
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (128, None, 512)          2787840   
_________________________________________________________________
lstm (LSTM)                  (128, None, 1024)         6295552   
_________________________________________________________________
dense (Dense)                (128, None, 5445)         5581125   
=================================================================
Total params: 14,664,517
Trainable params: 14,664,517
Non-trainable params: 0
_________________________________________________________________

这里的output指的是文本当中每个字的机率值,然後输出贿选机率最大的那个字

现在运行这个模型,看看它是否按预期运行。

首先检查输出的形状:

for input_example_batch, target_example_batch in ds.take(1):
  example_batch_predictions = model(input_example_batch)
  print(example_batch_predictions.shape, "# (batch_size, sequence_length, w)")
看起来模型没问题了,接下来定义优化器(optimizer)和损失函数(loss_function)

宣告一个function来作为损失函数(loss_function),本文的问题可以被视为一个有 w 个分类(字)的问题。而要定义分类问题的损失相对简单,使用 sparse_categorical_crossentropy 是个不错的选择

def loss(y_true, y_pred):
    return tf.keras.losses.sparse_categorical_crossentropy(y_true, y_pred, from_logits=True)

优化器(optimizer) 使用不论在任何任务中,表现都不错的 tf.keras.optimizers.Adam ,除了学习率(learning_rate)之外,其他采用默认参数,并使用 tf.keras.Model.compile 方法配置训练步骤,以及损失函数

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE), loss=loss
)

开始训练

终於到重头戏,训练的部分啦! 我们使用 fit 来训练,并且把资料 dsepochs,作为fit的参数
EPOCHS = 30 
history = model.fit(
    ds,
    epochs=EPOCHS, 
)
配置检查点
# 储存训练完成的模型
model.save('/content/gdrive/My Drive/LAB/IT_post/model_01.h5')
keras的model再训练时,会有像tqdm的进度条,看到模型一点一滴的训练,有没有觉得很有成就感阿~
Epoch 1/10
2668/2668 [==============================] - 694s 260ms/step - loss: 3.3912
Epoch 2/10
2668/2668 [==============================] - 695s 260ms/step - loss: 2.5633
Epoch 3/10
2668/2668 [==============================] - 699s 262ms/step - loss: 2.3310
Epoch 4/10
2668/2668 [==============================] - 696s 261ms/step - loss: 2.2017
Epoch 5/10
2668/2668 [==============================] - 693s 260ms/step - loss: 2.1163
Epoch 6/10
2668/2668 [==============================] - 690s 259ms/step - loss: 2.0567
Epoch 7/10
2668/2668 [==============================] - 689s 258ms/step - loss: 2.0134
Epoch 8/10
2668/2668 [==============================] - 690s 259ms/step - loss: 1.9830
Epoch 9/10
2668/2668 [==============================] - 692s 259ms/step - loss: 1.9613
Epoch 10/10
  73/2668 [..............................] - ETA: 11:07 - loss: 2.3482

建构生成的模型

跟训练时一样的超参数,只差在 BATCH_SIZE 为 1
EMBEDDING_DIM = 512
RNN_UNITS = 1024
BATCH_SIZE = 1

# 定义生成的模型
infer_model = tf.keras.Sequential()
infer_model.add(tf.keras.layers.Embedding(input_dim=w, output_dim=EMBEDDING_DIM, batch_input_shape=[BATCH_SIZE, None]))
infer_model.add(tf.keras.layers.LSTM(units=RNN_UNITS, return_sequences=True, stateful=True))
infer_model.add(tf.keras.layers.Dense(w))

# 载入已储存模型之权重
infer_model.load_weights('/content/gdrive/My Drive/LAB/IT_post/model_01.h5')
infer_model.build(tf.TensorShape([1, None]))

这边可以看到,我们的模型batch_size设成1,因为我们一次只能读一个段落,而模型再训练的时候因为要平行运算,因此batch_size才会设大於1

:::success
batch_size会尽量设成2的n次方,因为电脑的记忆体配置是用二进制去做运算
此举会稍微加速(可以实验看看)
:::

这里我们使用 load_weights 作为读取权重

build 会重建模型架构,而 tf.TensorShape 会将BATCH_SIZE(维度)固定为1

:::info
因为循环神经网路传递状态的方式,一旦建好模型,BATCH_SIZE 就不能做变动了。但在实际生成文章时,我们需要让 BATCH_SIZE 等於 1
:::

检查模型

infer_model.summary()

生成文章

因为我们在训练时是用数值表示,生成也会是数值,因此我们要写一段程序去将数值返回文字

接下来是生成模型的函式

text_generated = '梦见老鼠'

# 代表「乔」的索引
a = 1 

for i in range(100):

  dream = tokenizer.texts_to_sequences([text_generated])[0]

  # 增加 batch 维度丢入模型取得预测结果後
  # 再度降维,拿掉 batch 维度
  input = tf.expand_dims(dream, axis=0)
  predictions = infer_model(input)
  predictions = tf.squeeze(predictions, 0)

  temperature = 1.0
  # 利用生成温度影响抽样结果
  predictions /= temperature

  # 从 4330 个分类值中做抽样
  # 取得这个时间点模型生成的中文字
  predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()

  input_eval = tf.expand_dims([predicted_id], 0)

  partial_texts = [ tokenizer.index_word[predicted_id] ]

  text_generated += partial_texts[0]
  
# 成功生成 解梦文字档 → text_generated
# 透过撷取重点解梦文字作呈现
print(text_generated.split('\n')[0].split('。')[0])

想更深入认识 AI . FREE Team ?

自由团队 官方网站:https://aifreeblog.herokuapp.com/
自由团队 Github:https://github.com/AI-FREE-Team/
自由团队 粉丝专页:https://www.facebook.com/AI.Free.Team/
自由团队 IG:https://www.instagram.com/aifreeteam/
自由团队 Youtube:https://www.youtube.com/channel/UCjw6Kuw3kwM_il39NTBJVTg/

文章同步发布於:自由团队部落格
(想看更多文章?学习更多AI知识?敬请锁定自由团队的频道!)


<<:  DAY29-如何与人协同工作与好好沟通-英文很重要,中文也很重要,你有注意过你的欧化中文吗?

>>:  [Day - 29] React Hooks useEffect 学习笔记

RPA应用技术交流║给UiPath开发者的线上大会 2021

大家好! 以下分享RPA领域龙头UiPath在端午节过後那周的线上大会 活动名称:UiPath De...

那些被忽略但很好用的 Web API / ImageCapture

疫情时代,视讯串流当头,用视讯镜头来做个线上摄影吧! 自从疫情爆发後,各行各业也开始进行居家办公,...

19. STM32-CAN-BUS (下)

结构体介绍 CAN_FilterTypeDef typedef struct { uint32_t ...

【Day 7】机器学习基本功(五)

误差(Error)来自什麽地方? 来自於偏差(Bias) 来自於方差(Variance) 假设我们需...

【Day25】闭包进阶:工厂模式及私有方法

我们先来看一段闭包程序码 function arrFunction() { const arr = ...