首页 > 技术文章 > Keras笔记

ysysyzz 2020-12-28 22:12 原文

一、keras基础

keras效率比tensorflow慢。

pip install keras==2.2.5 ,对应tensorflow 1.14.0

import keras 出现 Using TensorFlow backend. 说明安装成功。

1、线性回归

import keras
import numpy as np
import matplotlib.pyplot as plt
# 按顺序构成的模型 Sequential
from keras.models import Sequential
# 全连接层 Dense
from keras.layers import Dense
# 100个随机点
x_data = np.random.rand(100)
noise = np.random.normal(0,0.01,x_data.shape)
y_data = x_data*0.1 + 0.2 +noise

plt.scatter(x_data,y_data)
plt.show()

# 构建一个顺序模型
model = Sequential()
# 在模型中添加一个全连接层 unit输出的维度(y) input_dim输入的维度(x)
model.add(Dense(units=1,input_dim=1))
# 编译模型 设置优化器std loss随机均方误差
model.compile(optimizer='sgd',loss='mse')

# 训练3001个批次
for step in range(3001):
    # 每次训练一个批次
    cost = model.train_on_batch(x_data,y_data)
    if step%500==0:
        print("cost:",cost)

# 打印权值和偏置值
W,b = model.layers[0].get_weights()
print('W=',W,'b=',b)

# 得到预测值
y_pred = model.predict(x_data)
plt.scatter(x_data,y_data)
plt.plot(x_data,y_pred,'r-',lw=3)
plt.draw()
cost: 0.36659515
cost: 0.03188929
cost: 0.008465202
cost: 0.0023047822
cost: 0.0006846307
cost: 0.00025854004
cost: 0.00014648054
W= [[0.12301245]] b= [0.18974891]

 

2、非线性回归

import keras
import numpy as np
import matplotlib.pyplot as plt
from keras.models import Sequential
from keras.layers import Dense,Activation
from keras.optimizers import SGD
x_data = np.linspace(-0.5,0.5,200)
noise = np.random.normal(0,0.02,x_data.shape)
y_data = np.square(x_data) + noise # 平方

plt.scatter(x_data,y_data)
plt.show()

 

 

model = Sequential()
# 1-10-1
model.add(Dense(units=10,input_dim=1))
# 非线性
model.add(Activation('tanh'))
# 两种加激活函数方法
model.add(Dense(units=1,activation='tanh'))

# 自己定义优化算法
sgd = SGD(lr=0.3)
model.compile(optimizer=sgd,loss='mse')

for step in range(3001):
    cost = model.train_on_batch(x_data,y_data)
    if step%500==0:
        print('cost:',cost)

y_pred = model.predict(x_data)

plt.scatter(x_data,y_data)
plt.plot(x_data,y_pred,'r-',lw=3)
plt.show()
cost: 0.013725451
cost: 0.004599485
cost: 0.0012144312
cost: 0.001094239
cost: 0.00035134255
cost: 0.0003509841
cost: 0.00035090686

 

 

3、mnist分类

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
# 载入数据
(x_train,y_train),(x_test,y_test) = mnist.load_data()

print(x_train.shape)
print(x_train.shape)

x_train = x_train.reshape(x_train.shape[0],-1)/255.0 # (60000,自动)/255做归一化
x_test = x_test.reshape(x_test.shape[0],-1)/255.0

# one-hot
y_train = np_utils.to_categorical(y_train,num_classes=10)
y_test = np_utils.to_categorical(y_test,num_classes=10)
(60000, 28, 28)
(60000, 28, 28)
# 创建模型,输入784神经元,输出10神经元
model = Sequential([
    # 偏置初始化为全1
    Dense(units=10,input_dim=784,bias_initializer='one',activation='softmax')
])
# 定义优化器和loss
sgd = SGD(lr=0.2)
model.compile(optimizer=sgd,loss='mse',metrics=['accuracy']) # 计算准确率

# 训练模型,fit的结果显示效果更好
model.fit(x_train,y_train,batch_size=32,epochs=10)

# 评估模型
loss,accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.013012470392603427
0.9176

 

4、交叉熵

model = Sequential([
    Dense(units=10,input_dim=784,bias_initializer='one',activation='softmax')
])
sgd = SGD(lr=0.2)
model.compile(optimizer=sgd,loss='categorical_crossentropy',metrics=['accuracy'])

model.fit(x_train,y_train,batch_size=32,epochs=10)

loss,accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.27709494123756884
0.922

 

5、dropout

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout
from keras.optimizers import SGD
model = Sequential([
    Dense(units=200,input_dim=784,bias_initializer='one',activation='tanh'),
    # drop使得训练和测试的结果更接近,解决过拟合问题
    Dropout(0.4), # 这一层40%神经元不工作
    Dense(units=100,bias_initializer='one',activation='tanh'),
    Dropout(0.4),
    Dense(units=10,bias_initializer='one',activation='softmax')
])
sgd = SGD(lr=0.2)
model.compile(optimizer=sgd,loss='categorical_crossentropy',metrics=['accuracy'])

model.fit(x_train,y_train,batch_size=32,epochs=10)

loss, accuracy = model.evaluate(x_train,y_train)
print(loss)
print(accuracy)

loss, accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.07171125439265742
0.9779166666666667
0.09752409034706652
0.9711

 

6、正则化

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout
from keras.optimizers import SGD
from keras.regularizers import l2
model = Sequential([
    # kernel_regularizer权值正则化 bias_regularizer偏置值正则化 activity_regularizer激活函数正则化
    Dense(units=200,input_dim=784,bias_initializer='one',activation='tanh',kernel_regularizer=l2(0.0003)),
    Dense(units=100,bias_initializer='one',activation='tanh',kernel_regularizer=l2(0.0003)),
    Dense(units=10,bias_initializer='one',activation='softmax',kernel_regularizer=l2(0.0003))
])
sgd = SGD(lr=0.2)
model.compile(optimizer=sgd,loss='categorical_crossentropy',metrics=['accuracy'])

model.fit(x_train,y_train,batch_size=32,epochs=10)

loss, accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.16635189125537872
0.9743

 

7、优化器

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout
from keras.optimizers import SGD,Adam
model = Sequential([
    # kernel_regularizer权值正则化 bias_regularizer偏置值正则化 activity_regularizer激活函数正则化
    Dense(units=200,input_dim=784,bias_initializer='one',activation='tanh'),
    Dense(units=100,bias_initializer='one',activation='tanh'),
    Dense(units=10,bias_initializer='one',activation='softmax')
])
# sgd = SGD(lr=0.2)
adam = Adam(lr=0.001)

model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

model.fit(x_train,y_train,batch_size=32,epochs=10)

loss, accuracy = model.evaluate(x_train,y_train)
print(loss)
print(accuracy)

loss, accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.013147175280582936
0.9955166666666667
0.08061714681818266
0.9775

adam速度快

 

8、CNN

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Convolution2D,MaxPooling2D,Flatten
from keras.optimizers import Adam
(x_train,y_train),(x_test,y_test) = mnist.load_data()
x_train = x_train.reshape(-1,28,28,1)/255.0
x_test = x_test.reshape(-1,28,28,1)/255.0
y_train = np_utils.to_categorical(y_train,num_classes=10)
y_test = np_utils.to_categorical(y_test,num_classes=10)
# 定义模型
model = Sequential()
# 第一个卷积层
# input_shape输入平面
# filters 卷积核/滤波器个数
# kernel_size 卷积窗口大小
# strides 步长
# padding same或valid
model.add(Convolution2D(
    input_shape = (28,28,1),
    filters = 32,
    kernel_size = 5,
    strides = 1,
    padding = 'same',
    activation = 'relu',
))
# 第一个池化层
model.add(MaxPooling2D(
    pool_size = 2,
    strides = 2,
    padding = 'same',
))
# 第二个卷积层
model.add(Convolution2D(64,5,strides=1,padding='same',activation='relu'))
# 第二个池化层
model.add(MaxPooling2D(2,2,padding='same'))
# 扁平化
model.add(Flatten())
# 全连接层
model.add(Dense(1024,activation='relu'))
# dropout
model.add(Dropout(0.4))
# 全连接层
model.add(Dense(10,activation='softmax'))

# 优化器
adam = Adam(lr=1e-4)
model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

# 训练
model.fit(x_train,y_train,batch_size=64,epochs=10)
# 评估
loss,accuracy = model.evaluate(x_test,y_test)
print('loss=',loss)
print('accuracy=',accuracy)
loss= 0.023454367653661757
accuracy= 0.9916

 

9、RNN

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.layers.recurrent import SimpleRNN,LSTM,GRU
from keras.optimizers import Adam
# 整个图片看成有28个序列
# 数据长度:一行28像素
input_size = 28
# 序列长度:28行
time_steps = 28
# 隐藏层cell个数
cell_size = 50

# 载入数据
(x_train,y_train),(x_test,y_test) = mnist.load_data()
# (60000,28,28)
x_train = x_train/255.0
x_test = x_test/255.0
y_train = np_utils.to_categorical(y_train,num_classes=10)
y_test = np_utils.to_categorical(y_test,num_classes=10)
# 创建模型
model = Sequential()
model.add(SimpleRNN(
    units=cell_size, # 输出
    input_shape=(time_steps,input_size),# 输入
))
model.add(Dense(10,activation='softmax'))

adam = Adam(lr=1e-4)
model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

# 训练
model.fit(x_train,y_train,batch_size=64,epochs=10)
# 评估
loss,accuracy = model.evaluate(x_test,y_test)
print('loss=',loss)
print('accuracy=',accuracy)

 

10、保存和载入

# 保存模型
model.save('model.h5') # HDF5文件,pip install h5py

 

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import SGD
from keras.models import load_model
# 载入数据
(x_train,y_train),(x_test,y_test) = mnist.load_data()

x_train = x_train.reshape(x_train.shape[0],-1)/255.0 # (60000,自动)/255做归一化
x_test = x_test.reshape(x_test.shape[0],-1)/255.0

# one-hot
y_train = np_utils.to_categorical(y_train,num_classes=10)
y_test = np_utils.to_categorical(y_test,num_classes=10)

# 载入模型
model = load_model('model.h5')

# 评估模型
loss,accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.013070186234917491
0.9179
# 可以继续训练
model.fit(x_train,y_train,batch_size=64,epochs=2)

loss,accuracy = model.evaluate(x_test,y_test)
print(loss)
print(accuracy)
0.012855883852392435
0.9196
# 前一种方式同时保存模型的结构和参数
# 如果想只保存参数:
model.save_weights('model_weights.h5')
model.load_weights('model_weights.h5')
# 如果只想保存结构:
json_string = model.to_json()
print(json_string)
from keras.models import model_from_json
model = model_from_json(json_string)

 

11、绘制网络结构

import numpy as np
from keras.datasets import mnist
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dense,Dropout,Convolution2D,MaxPooling2D,Flatten
from keras.optimizers import Adam
from keras.utils.vis_utils import plot_model
import matplotlib.pyplot as plt
# install pydot and graphviz
(x_train,y_train),(x_test,y_test) = mnist.load_data()
x_train = x_train.reshape(-1,28,28,1)/255.0
x_test = x_test.reshape(-1,28,28,1)/255.0
y_train = np_utils.to_categorical(y_train,num_classes=10)
y_test = np_utils.to_categorical(y_test,num_classes=10)

# 定义模型
model = Sequential()
# 第一个卷积层
# input_shape输入平面
# filters 卷积核/滤波器个数
# kernel_size 卷积窗口大小
# strides 步长
# padding same或valid
model.add(Convolution2D(
    input_shape = (28,28,1),
    filters = 32,
    kernel_size = 5,
    strides = 1,
    padding = 'same',
    activation = 'relu',
))
# 第一个池化层
model.add(MaxPooling2D(
    pool_size = 2,
    strides = 2,
    padding = 'same',
))
# 第二个卷积层
model.add(Convolution2D(64,5,strides=1,padding='same',activation='relu'))
# 第二个池化层
model.add(MaxPooling2D(2,2,padding='same'))
# 扁平化
model.add(Flatten())
# 全连接层
model.add(Dense(1024,activation='relu'))
# dropout
model.add(Dropout(0.4))
# 全连接层
model.add(Dense(10,activation='softmax'))

# 优化器
# adam = Adam(lr=1e-4)
# model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

# # 训练
# model.fit(x_train,y_train,batch_size=64,epochs=10)
# # 评估
# loss,accuracy = model.evaluate(x_test,y_test)
# print('loss=',loss)
# print('accuracy=',accuracy)
plot_model(model,to_file='model.png',show_shapes=True,show_layer_names=False,rankdir='TB')
# 查看图片
plt.figure(figsize=(10,10))
img = plt.imread('model.png')
plt.imshow(img)
plt.axis('off')
plt.show()

 

二、Keras实战

1、猫狗图像分类

1.1 数据预处理

文件结构:train---cat 1000

       ---dog 1000

     test---cat 500

       ---dog 500

from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img
# 对图片进行处理来扩大数据集
'''
rotation_range是一个0~180的度数,用来指定随机选择图片的角度。
width_shift和height_shift用来指定水平和竖直方向随机移动的程度,这是两个0-1之间的比
rescale值将在执行其他处理前乘到整个图像上,我们的图像在RGB通道都是0-255的整数,这样的操作可能使图像的值过高或过低,
所以我们将这个值定为01之间的数。
shear_range是用来进行剪切变换的程度,参考剪切变换
zoom_range用来进行随机的放大
horizontal_flip随机的对图片进行水平翻转,这个参数适用于水平翻转不影响图片语义的时候
fill_mode用来指定当需要进行像素填充,如旋转,水平和竖直位移时,如何填充新出现的像素
'''
datagen = ImageDataGenerator(
    rotation_range=40, # 随机旋转0-40之间的角度
    width_shift_range=0.2, # 随机水平平移
    height_shift_range=0.2, # 随机竖直平移
    rescale=1./255, # 数值归一化(图片数值0-255之间,所以除以255做归一化)
    shear_range=0.2, # 原始图片中裁剪一部分
    zoom_range=0.2, # 随机放大
    horizontal_flip=True, # 水平翻转
    fill_mode='nearest', # 填充方式
)
# 载入图片
img = load_img('data/catdog/train/cat/cat.1.jpg')
x = img_to_array(img)
print(x.shape)
x = x.reshape((1,)+x.shape) # 生成图片时需要传入四维格式,第一个维度代表batch-size,1张图片就是1
print(x.shape)
(280, 300, 3)
(1, 280, 300, 3)
i = 0
# flow随机生成20批/张图片,save_prefix前缀
for batch in datagen.flow(x, batch_size=1, save_to_dir='data/catdog/temp',save_prefix='cat',save_format='jpeg'):
    i+=1
    if i>20:
        break

 

1.2 简单CNN

from keras.models import Sequential
from keras.layers import Convolution2D,MaxPooling2D,Activation,Dropout,Flatten,Dense
from keras.optimizers import Adam
from keras.preprocessing.image import ImageDataGenerator
import os
# 定义模型
model = Sequential()
model.add(Convolution2D(input_shape=(150,150,3),filters=32,kernel_size=3,strides=1,padding='same',activation='relu'))
model.add(Convolution2D(filters=32,kernel_size=3,strides=1,padding='same',activation='relu'))
model.add(MaxPooling2D(pool_size=2,strides=2,padding='valid'))

model.add(Convolution2D(filters=64,kernel_size=3,strides=1,padding='same',activation='relu'))
model.add(Convolution2D(filters=64,kernel_size=3,strides=1,padding='same',activation='relu'))
model.add(MaxPooling2D(pool_size=2,strides=2,padding='valid'))

model.add(Flatten())
model.add(Dense(64,activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(2,activation='softmax'))

adam = Adam(lr=1e-4)
model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

# 展示模型结构
model.summary()

# 训练集数据生成
train_datagen = ImageDataGenerator(
    rescale=1./255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
)
# 测试集数据生成
test_datagen = ImageDataGenerator(rescale=1./255)
'''
flow_from_directory:

directory: 目标文件夹路径,对于每一个类,该文件夹都要包含一个子文件夹.子文件夹中任何JPG、PNG、BNP、PPM的图片都会被生成器使用.

target_size: 整数tuple,默认为(256, 256). 图像将被resize成该尺寸

color_mode: 颜色模式,为"grayscale","rgb"之一,默认为"rgb".代表这些图片是否会被转换为单通道或三通道的图片.

classes: 可选参数,为子文件夹的列表,如['dogs','cats']默认为None. 若未提供,则该类别列表将从directory下的子文件夹名称/结构自动推断。
每一个子文件夹都会被认为是一个新的类。(类别的顺序将按照字母表顺序映射到标签值)。通过属性class_indices可获得文件夹名与类的序号的对应字典。

class_mode: "categorical", "binary", "sparse"或None之一. 默认为"categorical. 该参数决定了返回的标签数组的形式, "categorical"会返回2D的
one-hot编码标签,"binary"返回1D的二值标签."sparse"返回1D的整数标签,如果为None则不返回任何标签, 生成器将仅仅生成batch数据, 
这种情况在使用model.predict_generator()和model.evaluate_generator()等函数时会用到.

batch_size: batch数据的大小,默认32
shuffle: 是否打乱数据,默认为True
seed: 可选参数,打乱数据和进行变换时的随机数种子

save_to_dir: None或字符串,该参数能让你将提升后的图片保存起来,用以可视化
save_prefix:字符串,保存提升后图片时使用的前缀, 仅当设置了save_to_dir时生效
save_format:"png"或"jpeg"之一,指定保存图片的数据格式,默认"jpeg"

flollow_links: 是否访问子文件夹中的软链接
'''
batch_size=32
# 训练数据
train_generator = train_datagen.flow_from_directory(
    'data/catdog/train', # 训练数据路径
    target_size=(150,150), # 统一图片大小
    batch_size=batch_size, # 批次大小
)
# 测试数据
test_generator = test_datagen.flow_from_directory(
    'data/catdog/test',
    target_size=(150,150),
    batch_size=batch_size,
)
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
# 统计文件个数
totalFileCount = sum([len(files) for root,dirs,files in os.walk('data/catdog/train')])
totalFileCount
2000
model.fit_generator(
    train_generator,
    steps_per_epoch=totalFileCount/batch_size,
    epochs=50,
    validation_data=test_generator,
    validation_steps=1000/batch_size,
)
# 保存模型
model.save('CNN1.h5')
Epoch 50/50
63/62 [==============================] - 97s 2s/step - loss: 0.3314 - acc: 0.8566 - val_loss: 0.5587 - val_acc: 0.7720

 

1.3 VGG16图片分类

使用vgg16的参数训练,再手动加上全连接层,训练并保存全连接层的参数

import numpy as np
from keras.applications.vgg16 import VGG16
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from keras.utils import np_utils
from keras.models import Sequential
from keras.layers import Dropout,Flatten,Dense
from keras.optimizers import Adam
# 载入预训练的VGG16模型,保存在C:\Users\.keras中
# weights使用imagenet数据集训练出的权值为基础
# include_top=False只有卷积和池化层(特征抽取),不包括全连接层(分类)
model = VGG16(weights='imagenet',include_top=False)
model.summary()

 

datagen = ImageDataGenerator(
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    rescale=1./255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
    fill_mode='nearest',
)
batch_size=32
train_steps=int((2000+batch_size-1)/batch_size)*10 #最后一批可能不够batchsize,所以加一次再整除,保证数据读完,
# 最后乘以10,所有图片均生成10张做了变换的图(相当于所有图片生成10轮)
test_steps = int((1000+batch_size-1)/batch_size)*10

generator = datagen.flow_from_directory(
    'data/catdog/train',
    target_size=(150,150),
    batch_size=batch_size,
    class_mode=None, # 不生成标签
    shuffle=False, # 不随机打乱
)
# 用参数现成的模型生成最后池化层的输出结果,且每张图生成10个变形
bottleneck_features_train=model.predict_generator(generator,train_steps)
print(bottleneck_features_train.shape)
# 保存训练集bottleneck结果
np.save(open('bottleneck_features_train.npy','wb'),bottleneck_features_train)

generator = datagen.flow_from_directory(
    'data/catdog/test',
    target_size=(150,150),
    batch_size=batch_size,
    class_mode=None,
    shuffle=False,
)
bottleneck_features_test = model.predict_generator(generator,test_steps)
print(bottleneck_features_test.shape)
# 保存测试集bottleneck结果
np.save(open('bottleneck_features_test.npy','wb'),bottleneck_features_test)
Found 2000 images belonging to 2 classes.
(20000, 4, 4, 512)
Found 1000 images belonging to 2 classes.
(10000, 4, 4, 512)
train_data = np.load(open('bottleneck_features_train.npy','rb'))
# 由于图片是顺序保存的,所以可以顺序创建标签
labels = np.array([0] * 1000 + [1] * 1000) # 1000个0和1000个1
train_labels = np.array([])
for _ in range(10): # 10倍的[0]和[1]
    train_labels = np.concatenate((train_labels,labels)) 

test_data = np.load(open('bottleneck_features_test.npy','rb'))
labels = np.array([0] * 500 + [1] * 500)
test_labels = np.array([])
for _ in range(10):
    test_labels = np.concatenate((test_labels,labels))

# 将创建的标签转成one-hot编码
train_labels = np_utils.to_categorical(train_labels,num_classes=2)
test_labels = np_utils.to_categorical(test_labels,num_classes=2)
model = Sequential()
model.add(Flatten(input_shape=train_data.shape[1:])) # 不要20000,只留下(4,4,512)
model.add(Dense(256,activation='relu')) 
model.add(Dropout(0.5))
model.add(Dense(2,activation='softmax'))

adam = Adam(lr=1e-4)
model.compile(optimizer=adam,loss='categorical_crossentropy',metrics=['accuracy'])

model.fit(train_data,train_labels,batch_size=batch_size,epochs=20,validation_data=(test_data,test_labels))
model.save_weights('bottleneck_fc_model.h5')
Epoch 20/20
20000/20000 [==============================] - 32s 2ms/step - loss: 0.0286 - acc: 0.9920 - val_loss: 0.5210 - val_acc: 0.8670

 

1.4 使用VGG16的Finetune

将上一步的参数训练结果和vgg16原有的参数一起,设置较低的学习率进行finetune。

import numpy as np
from keras.applications import VGG16
from keras.applications.vgg16 import preprocess_input
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dropout,Dense,Flatten
from keras.optimizers import SGD
import os
# 载入预训练的vgg16模型,不包括全连接层,输入格式为(150,150,3)
vgg16_model = VGG16(weights='imagenet',include_top=False,input_shape=(150,150,3))
vgg16_model.summary()
# 搭建全连接层
top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16_model.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(2,activation='softmax'))

# 载入训练过的权值
top_model.load_weights('bottleneck_fc_model.h5')

model = Sequential()
model.add(vgg16_model)
model.add(top_model)
train_datagen = ImageDataGenerator(
    rescale=1./255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True,
)
test_datagen = ImageDataGenerator(rescale=1./255)
batch_size=32
train_generator = train_datagen.flow_from_directory(
    'data/catdog/train',
    target_size=(150,150),
    batch_size=batch_size,
)
test_generator = test_datagen.flow_from_directory(
    'data/catdog/test',
    target_size=(150,150),
    batch_size=batch_size,
)
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
model.compile(
    loss='categorical_crossentropy',
    optimizer=SGD(lr=1e-4,momentum=0.9), # 因为是做微调,之前参数已经训练好了,所以学习率要设置得比较小
    metrics=['accuracy'])
# 统计文件个数
totalFileCount = sum([len(files) for root,dirs,files in os.walk('data/catdog/train')])

model.fit_generator(
    train_generator,
    steps_per_epoch=totalFileCount/batch_size,
    epochs=10, # 可以设长一些
    validation_data=test_generator,
    validation_steps=1000/batch_size,
)

 

2、图像风格转换

2.1 原理

Image Style Transfer Using Convolutional Neural Networks - CVPR2016

(1)

先准备好一个已经训练好的网络如vgg16,将图片a放进去训练后得到一个特征图,对特征图进行Gram矩阵的变换,得到的结果A可以认为代表这张图片的风格。

x表示要进行变换的图片,把它放到同样的网络中进行计算,也进行Gram变换得到结果G。

对比G和A,越相近说明图片越相似。通过优化代价函数Lstyle 来使L个卷积层的输出得到的特征接近。

但是这个优化过程不是通过改变模型的权值,而是改变输入的图片x像素点的值。

(2)

右边部分的输入p是输入的图片内容,只需要某一层的模型的结果作为内容的输出。

在同一层的输出F和P不需要做Gram变换,直接进行像素之间的点对点的比较,使得它们尽可能相似。

(3)

左边比较的是风格,右边比较的是内容。

优化的目的是使得中间找到的图片x和左边的风格越相似越好且和右边的内容越相似越好。

 

loss函数由三项构成:
(1)风格损失,即Gram矩阵差的平方。代表待优化图片与风格图片的相似度。
(2)内容损失,即内容图片和待优化图片的差的平方。代表待优化图片与内容图片的相似度。
(3)待优化图片的正则项,用来使得生成的图片更平滑自然。
这三项内容通过适当的加权组合起来。

 

为什么Gram矩阵能表征图像的风格呢?
feature map是提取到的抽象特征,而Gram矩阵,就是各个feature map两两做内积,其实就是计算各个feature map的两两相关性。
以梵高的星空举例:
某一层中有一个滤波器专门检测尖尖的部位,另一个滤波器专门检测黑色。有一个滤波器专门检测圆形,另一个滤波器专门检测金黄色。对于梵高的星空来说,“尖尖的”和“黑色”经常一起出现,它们的相关性比较高。而“圆圆的”和“金黄色”经常一起出现,它们的相关性比较高。因此在风格转移的时候,其他的图片也会去寻找这种搭配。

 

2.2 代码详解

import time
import numpy as np
import matplotlib.pyplot as plt
from keras.preprocessing.image import load_img, img_to_array

# from scipy.misc import imsave # 已弃用,会报错
import imageio
imsave = imageio.imsave
from scipy.optimize import fmin_l_bfgs_b

from keras.applications import vgg16
# Keras并不处理如张量乘法、卷积等底层操作,需要调用backend
from keras import backend as K
# 设置参数
base_image_path = 'image/content/tubingen.jpg' # 图片内容
style_reference_image_path = 'image/style/starry_night.jpg' # 图片风格
result_prefix = 'image/output/output' # 输出路径
iterations = 30 # 迭代次数
total_variation_weight = 8.5e-5 # 图片平滑处理的权值
style_weight = 1.0 # 风格的权值
content_weight = 0.025 # 内容的权值

# 设置产生图片的大小(为了后面将原来的图片进行等比缩放)
width, height = load_img(base_image_path).size # 获取原图宽高
img_nrows = 200 # 设置行
img_ncols = int(width * img_nrows / height) # 设置列,列数是等比例计算的
# 图片预处理
def preprocess_image(image_path):
    # 使用Keras内置函数读入图片并设置为前面指定的长宽
    img = load_img(image_path, target_size=(img_nrows, img_ncols)) 
    # 转为numpy array格式
    img = img_to_array(img)
    #:keras中tensor是4维张量,所以给数据加上一个维度
    img = np.expand_dims(img, axis=0) # 如三维分别为(2,3,4),加在0位置就变成了(1,2,3,4),即1*2*3*4的张量
    
    # vgg提供的预处理,主要完成(1)减去颜色均值(2)RGB转BGR(3)维度调换三个任务。
    # 减去颜色均值可以提升效果,就是三原色分别减去三原色的平均值。
    # RGB转BGR是因为vgg16的权重是在caffe上训练的,caffe的彩色维度顺序是BGR。
    # 维度调换是要根据系统设置的维度顺序theano/tensorflow将通道维调到正确的位置,如theano的通道维应为第二维,tensorflow通道在第四维
    # tensorflow (batch,width,height,channel)
    img = vgg16.preprocess_input(img) 
    return img
# 预处理的反向操作
def deprocess_image(x):
    x = x.reshape((img_nrows, img_ncols, 3)) 
    # 加上颜色均值
    x[:, :, 0] += 103.939
    x[:, :, 1] += 116.779
    x[:, :, 2] += 123.68 
    # 'BGR'->'RGB'
    x = x[:, :, ::-1]
    # 颜色值限定在0-255之间,大于255的就让它等于255
    x = np.clip(x, 0, 255).astype('uint8') 
    return x
# 设置Gram矩阵的计算图,首先用batch_flatten将输出的feature map扁平化,
# 然后自己跟自己的转置矩阵做乘法。注意这里的输入是深度学习网络某一层的输出值。
def gram_matrix(x): 
    # permute_dimensions按照给定的模式重排一个张量,如(14,14,512)->(512,14,14)
    # batch_flatten将一个n阶张量转变为2阶张量,其第一维度保留不变,
    # 这里的扁平化主要是保留特征图的个数,让二维的特征图变成一维,如(512,14*14)
    features = K.batch_flatten(K.permute_dimensions(x, (2, 0, 1)))
    # 格拉姆矩阵,矩阵乘以它的转置,(512,196)*(196,512)=(512,512)
    gram = K.dot(features, K.transpose(features))
    return gram

# 设置风格loss计算方式,以风格图片和待优化的图片的某一卷积层的输出作为输入。
# 计算他们的Gram矩阵,然后计算两个Gram矩阵的差的平方,除以一个归一化值
def style_loss(style, combination): 
    # 检查一下是不是三维的(width,height,channel)
    assert K.ndim(style) == 3
    assert K.ndim(combination) == 3
    # 计算gram矩阵
    S = gram_matrix(style)
    C = gram_matrix(combination)
    # 定义通道数和图片大小
    channels = 3
    size = img_nrows * img_ncols
    # K.sum(K.square(S - C)) 差值平方再累加,(4. * (channels ** 2) * (size ** 2)) 论文中提到的归一化处理方法
    return K.sum(K.square(S - C)) / (4. * (channels ** 2) * (size ** 2))

# 设置内容loss计算方式,以内容图片和待优化的图片的representation为输入,计算他们差的平方。像素级对比
def content_loss(base, combination):
    return K.sum(K.square(combination - base))

# 施加全变差正则,全变差正则化常用于图片去噪,可以使生成的图片更加平滑自然。
def total_variation_loss(x): 
    assert K.ndim(x) == 4
    a = K.square(x[:, :img_nrows-1, :img_ncols-1, :] - x[:, 1:, :img_ncols-1, :])
    b = K.square(x[:, :img_nrows-1, :img_ncols-1, :] - x[:, :img_nrows-1, 1:, :])
    return K.sum(K.pow(a + b, 1.25))
# 读入内容和风格图,包装为Keras张量,这是一个4维张量
base_image = K.variable(preprocess_image(base_image_path)) # 内容图
style_reference_image = K.variable(preprocess_image(style_reference_image_path)) # 风格图 

# 初始化一个待优化图片的占位符,这个地方待会儿实际跑起来的时候把噪声图片或者内容图片填进来。
combination_image = K.placeholder((1, img_nrows, img_ncols, 3)) 

# 将上面三个张量串联到一起,形成一个形状为(3,img_nrows,img_ncols,3)的张量
input_tensor = K.concatenate([base_image, style_reference_image, combination_image], axis=0) 
# 载入模型,输入就是上面的三合一的张量
model = vgg16.VGG16(input_tensor=input_tensor, weights='imagenet', include_top=False)
print('Model loaded.')

# 这是一个字典,建立了层名称到层输出张量的映射,通过这个字典我们可以通过层的名字来获取其输出张量。
# 使用model.get_layer(layer_name).output的效果也是一样的。
outputs_dict = dict([(layer.name, layer.output) for layer in model.layers]) # 就是把每一层的名字和输出存到字典里

# 初始化loss值
loss = K.variable(0.) 

# 计算内容损失
layer_features = outputs_dict['block5_conv2'] # 这里只取了block5_conv2这一层的输出进行对比,取多层输出效果变化不大
# 记得我们把输入做成了(3,nb_rows,nb_cols, 3)这样的张量,0号位置对应内容图像的输出,1号是风格图像的,2号位置是待优化的图像的。
base_image_features = layer_features[0, :, :, :] 
combination_features = layer_features[2, :, :, :] # 计算内容损失取内容图像和待优化图像即可 
loss += content_weight * content_loss(base_image_features, combination_features) # 计算内容损失,加到总的loss上

# 计算风格损失
# 与上面的过程类似,只是对多个层的输出作用而已,求出各个层的风格loss,相加求平均即可。
feature_layers = ['block1_conv1', 'block2_conv1',
                  'block3_conv1', 'block4_conv1',
                  'block5_conv1']
for layer_name in feature_layers:
    layer_features = outputs_dict[layer_name] # 循环提取每一个层的输出
    style_reference_features = layer_features[1, :, :, :] # 计算风格损失取风格图像和待优化图像即可 
    combination_features = layer_features[2, :, :, :]
    sl = style_loss(style_reference_features, combination_features) # 计算风格损失
    loss += (style_weight / len(feature_layers)) * sl # 要除层数

# 求全变差正则,加入总loss中
loss += total_variation_weight * total_variation_loss(combination_image) # 传入待优化图片
Model loaded.
# 得到loss函数关于待优化图片combination_image的梯度
grads = K.gradients(loss, combination_image) 

# 我们希望同时得到梯度和损失,所以这两个都应该是计算图的输出
# 0号位置是loss,1号位置是grads
outputs = [loss]
outputs += grads

# 编译计算图。前面的代码都在规定输入输出的计算关系,到这里才将计算图编译了。
# 这条语句以后,f_outputs就是一个可用的Keras函数,给定一个输入张量,就能获得其loss值和梯度了。
# 第一个参数输入是前面定义的待优化图片的占位符,第二个参数输出,一次计算同时可以得到loss和grads值
f_outputs = K.function([combination_image], outputs) 
# 获取loss和grads
def eval_loss_and_grads(x):
    # 把输入reshape成矩阵
    x = x.reshape((1, img_nrows, img_ncols, 3))
    # 这里调用了我们刚定义的计算图
    outs = f_outputs([x])
    # 计算图的结果outs是一个长为2的tuple,0号位置是loss,1号位置是grads。
    loss_value = outs[0]
    grad_values = outs[1].flatten().astype('float64') # 把grads扁平化
    return loss_value, grad_values

# 定义了两个方法,一个用于返回loss,一个用于返回grads
class Evaluator(object):
    def __init__(self):
        # 初始化损失值和梯度值
        self.loss_value = None
        self.grads_values = None

    def loss(self, x):
        # 调用函数得到梯度值和损失值,返回损失值,并将梯度值保存在成员变量self.grads_values中
        assert self.loss_value is None
        loss_value, grad_values = eval_loss_and_grads(x)
        self.loss_value = loss_value
        self.grad_values = grad_values
        return self.loss_value

    def grads(self, x):
        # 这个函数不用做任何计算,只返回成员变量self.grads_values的值
        assert self.loss_value is not None
        grad_values = np.copy(self.grad_values)
        self.loss_value = None
        self.grad_values = None
        return grad_values
evaluator = Evaluator()

# 使用内容图片作为待优化图片的初始值,待优化图片同样也要初始化
x = preprocess_image(base_image_path)

# 显示原始内容图片
img = load_img(base_image_path, target_size=(img_nrows, img_ncols)) 
plt.imshow(img)
plt.axis('off') # 去掉坐标轴
plt.show()

for i in range(iterations):
    print('Start of iteration', i)
    start_time = time.time()
    # 使用(L-BFGS)算法来最小化loss的值(拟牛顿法 在图像融合上效果好)
    # 参数1:传入一个带返回值的函数,然后最小化返回值
    # 参数2:初始值(初始图片),做了扁平化
    # 参数3:传入一个带返回值的函数,返回值是梯度
    # 参数4:迭代次数
    # 返回值1:优化后的值(改变的图片)
    # 返回值2:loss值
    # 返回值3:计算过程的一些信息
    x, min_val, info = fmin_l_bfgs_b(evaluator.loss, x.flatten(), fprime=evaluator.grads, maxfun=20)
    print('Current loss value:', min_val)
    
    # 拷贝一份x,传入反向预处理操作,加上颜色均值,'BGR'->'RGB'
    img = deprocess_image(x.copy())
    # 保存每一张产生的新图片
    fname = result_prefix + '_at_iteration_%d.png' % i # 起名字
    print('Image saved as', fname)
    imsave(fname, img) # 保存图片
    
    # 计算迭代1次花费的时间
    end_time = time.time()
    print('Iteration %d completed in %ds' % (i, end_time - start_time))
    
    # 显示这张图片
    plt.imshow(img)
    plt.axis('off')
    plt.show()

 

 

3 Word2Vec与情感分类

3.1 Word2Vec

当我们分析图片或者语音时,通常都是在分析密集的、高维度的数据集,我们所需要的全部信息都存储在原始数据中。

如自然语言处理问题通常会做分词,给每个词编号。但是这些编号是没有规律的,我们从编号中不能得到词与词之间的相关性。

连续词袋模型(CBOW):根据词的上下文词汇来预测目标词汇,例如上下文词汇是“今天早餐吃__”,要预测的的目标可能是“面包”。

Skip-Gram模型:与CBOW相反,通过目标词汇来预测上下文词汇,例如目标词汇是“早餐”,要预测的上下文词汇可能是“今天”和“吃面包”。

 

对于这两种模型的训练,可能很容易想到使用softmax作为输出层来训练网络,但是这样做的计算量是巨大的。假设已知上下文,要预测目标词汇,假设总共有50000个词汇,那么每一次训练都要计算输出层的50000个概率值。

所以训练Word2Vec模型通常使用噪声对比估计(Noise Contrastive Estimation)。NCE的使用方法是把上下文h对应的正确的目标词汇标记为正样本(D=1),然后再抽取一些错误的词汇作负样本(D=0)。然后最大化目标函数的值。

 

当真实的目标单词被分配到较高的概率,同时噪声单词的概率很低时,目标函数也就达到最大值了。由于只需要计算挑出来的k个噪声词,而不是整个语料库,所以训练速度会很快。

图形化:

 

 3.2 CNN在自然语言处理中的应用

CNN应用于NLP的任务,处理的往往是以矩阵形式表达的句子或文本。矩阵中的每一行对应一个分词元素,一般是一个单词,也可以是一个字符 。也就是说每一行都是一个词或者字符的向量。假设我们一共有10个词,每个词都用128维的向量来表示,那么我们就可以得到一个10×128维的矩阵,这个矩阵就可以看做是一幅图像。

 

 

卷积核的列数和词向量维数是一样的(d=5),这里取了三种不同的卷积核行数,分别为2,3,4,每种有2个。

步长为1时,得到的feature map是n×1的,然后做maxpooling,再拼接起来。

最后是一个全连接层,做分类(2类)

3.3 函数式模型

序贯(Sequential)模型

# 定义序贯模型
model = Sequential()

# 第一个卷积层
model.add(Conv2D(
    input_shape = (28,28,1),
    filters = 32,
    kernel_size = 5,
    strides = 1,
    padding = 'same',
    activation = 'relu'
))
# 第一个池化层
model.add(MaxPooling2D(
    pool_size = 2,
    strides = 2,
    padding = 'same',
))
# 第二个卷积层
model.add(Conv2D(64,5,strides=1,padding='same',activation = 'relu'))
# 第二个池化层
model.add(MaxPooling2D(2,2,'same'))
# 把第二个池化层的输出扁平化为1维
model.add(Flatten())
# 第一个全连接层
model.add(Dense(1024,activation = 'relu'))
# Dropout
model.add(Dropout(0.5))
# 第二个全连接层
model.add(Dense(10,activation='softmax'))

函数式(Functional)模型

# 定义函数式模型
inputs = Input(shape=(28,28,1)) # from keras.layers import
x = Conv2D(filters=32, kernel_size=5, padding='same', activation='relu')(inputs)
x = MaxPooling2D(pool_size = 2)(x)
x = Conv2D(filters=64, kernel_size=5, padding='same', activation='relu')(x)
x = MaxPooling2D(pool_size = 2)(x)
x = Flatten()(x)
x = Dense(1024,activation = 'relu')(x)
x = Dropout(0.5)(x)
predictions = Dense(10,activation='softmax')(x)
model = Model(inputs=inputs, outputs=predictions) # from keras.models import Model,这里要指定输入和输出

函数式模型举例:Inception

 

序贯模型定义不了复杂的结构,从下往上多个通道。

# Inception
from keras.layers import Conv2D, MaxPooling2D, Input

input_img = Input(shape=(256, 256, 3))

tower_1 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_1 = Conv2D(64, (3, 3), padding='same', activation='relu')(tower_1)

tower_2 = Conv2D(64, (1, 1), padding='same', activation='relu')(input_img)
tower_2 = Conv2D(64, (5, 5), padding='same', activation='relu')(tower_2)

tower_3 = MaxPooling2D((3, 3), strides=(1, 1), padding='same')(input_img)
tower_3 = Conv2D(64, (1, 1), padding='same', activation='relu')(tower_3)

output = keras.layers.concatenate([tower_1, tower_2, tower_3], axis=1)

3.4 情感分类CNN

import pandas as pd 
import numpy as np 
import jieba # pip install jieba -i https://pypi.tuna.tsinghua.edu.cn/simple

from keras.layers import Dense, Input, Flatten, Dropout
from keras.layers import Conv1D, MaxPooling1D, Embedding, concatenate
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Model
# 读入数据
neg=pd.read_excel('data/neg.xls',header=None) # pip install xlrd
pos=pd.read_excel('data/pos.xls',header=None)
neg.head(5)
neg[:5]

#合并语料
pn = pd.concat([pos,neg],ignore_index=True) 
#计算语料数目
neglen = len(neg)
print(neglen)
poslen = len(pos) 
print(poslen)
10428
10677
# 定义分词函数
cw = lambda x: list(jieba.cut(x))
# 在pn加一列words,取pn的第0列,应用cw函数,作为words列的值
pn['words'] = pn[0].apply(cw)

pn

# 数据最多的一行的词汇数
max_document_length = max(len(x) for x in pn['words'])
max_document_length
# 设置一个评论最多1000个词,超过的部分会被截掉,不足的部分会补上
max_document_length = 1000
1804
# 把每一行数据转换成一个字符串,用空格隔开
texts = [' '.join(x) for x in pn['words']]
texts[-2] # 查看倒数第二条评论
'宝贝 不错 , 物流 也 不错 , 售后 差 ,'
# 实例化分词器,设置字典中最大词汇数为30000
tokenizer = Tokenizer(num_words=30000)
# 传入我们的训练数据,建立词典
tokenizer.fit_on_texts(texts) 

# 把词转换为编号,词的编号根据词频设定,频率越大,编号越小
sequences = tokenizer.texts_to_sequences(texts) 
# 把所有句子序列设定为1000的长度,超过1000的部分舍弃,不到1000则补0
sequences = pad_sequences(sequences, maxlen=1000, padding='post')
sequences = np.array(sequences)

# 提取词对应编号的字典,词是字典的键,编号是字典的值
dict_text = tokenizer.word_index
print(dict_text[''])

sequences[-2]
9
array([1318,   24,    1, 1482,    9,   24,    1,  909,  156,    1,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,......])
# 定义标签
positive_labels = [[0, 1] for _ in range(poslen)]
negative_labels = [[1, 0] for _ in range(neglen)]
y = np.concatenate([positive_labels, negative_labels], 0)

# 打乱数据
np.random.seed(10)
shuffle_indices = np.random.permutation(np.arange(len(y))) # 生成打乱的索引
x_shuffled = sequences[shuffle_indices] # 根据打乱的索引顺序得到打乱的数据
y_shuffled = y[shuffle_indices] # x和y打乱的顺序一样

# 数据集切分为两部分
test_sample_index = -1 * int(0.1 * float(len(y))) # 测试集占0.1
x_train, x_test = x_shuffled[:test_sample_index], x_shuffled[test_sample_index:]
y_train, y_test = y_shuffled[:test_sample_index], y_shuffled[test_sample_index:]
# 以上都是对数据的预处理
# 定义函数式模型
# 模型输入,每个数组有1000词
sequence_input = Input(shape=(1000,))
# Embedding层,30000表示30000个词,每个词对应的向量为128维,序列长度为1000
embedding_layer = Embedding(30000,
                            128,
                            input_length=1000)
embedded_sequences = embedding_layer(sequence_input) # 每个句子变成了1000行128列的词向量矩阵

# 卷积核大小为3
cnn1 = Conv1D(filters=32, kernel_size=3, activation='relu')(embedded_sequences) # 一维卷积,一次只对3行做卷积
cnn1 = MaxPooling1D(pool_size=5)(cnn1)
cnn1 = Conv1D(filters=32, kernel_size=3, activation='relu')(cnn1)
cnn1 = MaxPooling1D(pool_size=5)(cnn1)
cnn1 = Conv1D(filters=32, kernel_size=3, activation='relu')(cnn1)
cnn1 = MaxPooling1D(pool_size=37)(cnn1)
cnn1 = Flatten()(cnn1)
# 卷积核大小为4
cnn2 = Conv1D(filters=32, kernel_size=4, activation='relu')(embedded_sequences)
cnn2 = MaxPooling1D(pool_size=5)(cnn2)
cnn2 = Conv1D(filters=32, kernel_size=4, activation='relu')(cnn2)
cnn2 = MaxPooling1D(pool_size=5)(cnn2)
cnn2 = Conv1D(filters=32, kernel_size=4, activation='relu')(cnn2)
cnn2 = MaxPooling1D(pool_size=36)(cnn2)
cnn2 = Flatten()(cnn2)
# 卷积核大小为5
cnn3 = Conv1D(filters=32, kernel_size=5, activation='relu')(embedded_sequences)
cnn3 = MaxPooling1D(pool_size=5)(cnn3)
cnn3 = Conv1D(filters=32, kernel_size=5, activation='relu')(cnn3)
cnn3 = MaxPooling1D(pool_size=5)(cnn3)
cnn3 = Conv1D(filters=32, kernel_size=5, activation='relu')(cnn3)
cnn3 = MaxPooling1D(pool_size=35)(cnn3)
cnn3 = Flatten()(cnn3)

# 合并,三个扁平化后的数据连接起来
merge = concatenate([cnn1, cnn2, cnn3], axis=1)
# 全链接层
x = Dense(128, activation='relu')(merge)
# Dropout层
x = Dropout(0.5)(x)
# 输出层
preds = Dense(2, activation='softmax')(x)

# 定义模型
model = Model(sequence_input, preds)
model.summary()
# 训练模型
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['acc'])

model.fit(x_train, y_train,
          batch_size=128,
          epochs=5,
          validation_data=(x_test, y_test))
Train on 18995 samples, validate on 2110 samples
Epoch 1/5
18995/18995 [==============================] - 200s 11ms/step - loss: 0.4767 - acc: 0.7507 - val_loss: 0.2315 - val_acc: 0.9128
Epoch 2/5
18995/18995 [==============================] - 172s 9ms/step - loss: 0.1450 - acc: 0.9504 - val_loss: 0.1731 - val_acc: 0.9341
Epoch 3/5
18995/18995 [==============================] - 174s 9ms/step - loss: 0.0526 - acc: 0.9848 - val_loss: 0.2411 - val_acc: 0.9261
Epoch 4/5
18995/18995 [==============================] - 165s 9ms/step - loss: 0.0221 - acc: 0.9953 - val_loss: 0.2266 - val_acc: 0.9341
Epoch 5/5
18995/18995 [==============================] - 158s 8ms/step - loss: 0.0106 - acc: 0.9982 - val_loss: 0.2556 - val_acc: 0.9336
# 预测
def predict(text):
    # 对句子分词
    cw = list(jieba.cut(text)) 
    word_id = []
    # 把词转换为编号
    for word in cw:
        try:
            temp = dict_text[word] # 从字典里找每个词的编号
            word_id.append(temp)
        except:
            word_id.append(0) # 字典里没有的词记0
    word_id = np.array(word_id)
    word_id = word_id[np.newaxis,:] # 只有一个句子,所以加个维度
    sequences = pad_sequences(word_id, maxlen=1000, padding='post') #填充到1000词
    result = np.argmax(model.predict(sequences)) # 预测结果[1,0]或[0,1]
    if(result==1):
        print("positive comment")
    else:
        print("negative comment")
predict("用起来很麻烦,颜色一洗就掉,下次不会买了")
negative comment
predict("味道好,香脆可口,下次还会买")
positive comment

 

3.5 情感分类LSTM

import pandas as pd
import numpy as np
import jieba

from keras.layers import Dense,Input,Flatten,Dropout,Embedding
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Model
neg = pd.read_excel('data/neg.xls',header=None)
pos = pd.read_excel('data/pos.xls',header=None)
pn = pd.concat([pos,neg],ignore_index=True)
neglen = len(neg)
poslen = len(pos)
print(neglen)
print(poslen)
10428
10677
cw = lambda x: list(jieba.cut(x))
pn['words'] = pn[0].apply(cw)
pn

 

max_document_length = max([len(x) for x in pn['words']])
max_document_length
1804
max_document_length = 1000
texts = [' '.join(x) for x in pn['words']]
texts[-2]
'宝贝 不错 , 物流 也 不错 , 售后 差 ,'
tokenizer = Tokenizer(num_words=30000)
'''
texts: (1)1个由字符串构成的列表,列表中的每个元素是一个字符串(表示一篇文本)
(2)1个由字符串类型构成的生成器(当文本语料很大而不能通过列表直接加载到内存中时使用生成器一次读取一个文本到内存)
(3)1个由字符串构成的列表的列表,此时一篇文本不再由一个字符串表示,而是由一个词语列表表示,词语列表中的每个元素是一个字符串
(表示一个词语),整个语料由多个词语列表表示。这种参数针对需要经过特定分词器输出的文本集,比如中文文本集。

'''
# 在调用texts_to_sequences或texts_to_matrix方法之前,必须先调用该方法构建词汇表。
tokenizer.fit_on_texts(texts)

# 将文本集中的每篇文本变换为词语索引序列。注意:只有属于词汇表中前num_words的词才被索引替换,其他词直接忽略。
# 返回值:词索引列表构成的文本集列表。
sequences = tokenizer.texts_to_sequences(texts) 
sequences = pad_sequences(sequences,maxlen=1000) # 不加padding='post',默认填充0在前面
sequences = np.array(sequences)
dict_text = tokenizer.word_index
dict_text['下次']
312
sequences[-2]
array([   0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,.......
      0,    0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
         1318,   24,    1, 1482,    9,   24,    1,  909,  156,    1])
positive_labels = [[0,1] for _ in range(poslen)]
negative_labels = [[1,0] for _ in range(neglen)]
y = np.concatenate([positive_labels,negative_labels],0)

np.random.seed(10)
shuffle_indices = np.random.permutation(np.arange(len(y)))
x_shuffled = sequences[shuffle_indices]
y_shuffled = y[shuffle_indices]

test_sample_index = -1 * int(0.1 * float(len(y)))
x_train, x_test = x_shuffled[:test_sample_index], x_shuffled[test_sample_index:]
y_train, y_test = y_shuffled[:test_sample_index], y_shuffled[test_sample_index:]
from keras.layers.wrappers import Bidirectional
from keras.layers.recurrent import LSTM
from keras.layers.recurrent import GRU
# 定义函数式模型
# 模型输入
sequence_input = Input(shape=(1000,))
# Embedding层,30000表示30000个词,每个词对应的向量为128维,序列长度为1000
embedding_layer = Embedding(30000,
                            128,
                            input_length=1000)
embedded_sequences = embedding_layer(sequence_input)

# 方法一,LSTM的输出为(batch, 10)
# 输出维度为10,也就是每个单词由128维输出10维的向量,可以获得1000个10维的词向量。
# dropout是输入和hidden之间的dropout,recurrent_dropout是hidden和hidden之间的dropout
# 最终的输出是第1000个词的10维向量,也就是(batch,10)
# 所以在上一步1000个词填充0时,0应该填充在句子前面,否则如果后面全是0,会削弱前面包含的有用信息
lstm1 = LSTM(10, dropout=0.2, recurrent_dropout=0.2)(embedded_sequences)
lstm1 = Dense(16, activation='relu')(lstm1)
lstm1 = Dropout(0.5)(lstm1)

# 方法二,LSTM的输出为(batch, 1000, 10)
# 增加了return_sequences=True参数,得到的结果是每一个词输出的10维向量,即(batch, 1000, 10)
# 这种方法在上一步0填充时,补在前面和后面都没有区别,因为所有词的输出结果都会被用到
# lstm1 = LSTM(10,return_sequences=True,dropout=0.2, recurrent_dropout=0.2)(embedded_sequences)
# lstm1 = Flatten()(lstm1) # 10000个数据,计算量比方法一大,准确率比方法一要好
# lstm1 = Dense(16, activation='relu')(lstm1)
# lstm1 = Dropout(0.5)(lstm1)

# 方法三
# 可以结合采用多种RNN模型
# Bidirectional指双向LSTM,会考虑词前后的内容信息
# lstm2 = Bidirectional(LSTM(10,return_sequences=True,dropout=0.2, recurrent_dropout=0.2))(embedded_sequences)
# lstm2 = Flatten()(lstm2)
# lstm2 = Dense(16, activation='relu')(lstm2)
# lstm2 = Dropout(0.5)(lstm2)

# GRU只有两个门,速度比LSTM要快,效果和LSTM差不多
# lstm3 = GRU(10,return_sequences=True,dropout=0.2, recurrent_dropout=0.2)(embedded_sequences)
# lstm3 = Flatten()(lstm3)
# lstm3 = Dense(16, activation='relu')(lstm3)
# lstm3 = Dropout(0.5)(lstm3)

# 把三个模型得到的结果拼接起来,即考虑多种信息,也有可能会提升准确率
# merge = concatenate([lstm1, lstm2, lstm3], axis=1)
# merge=Dense(16,activation='relu')(merge)
# merge=Dropout(0.5)(merge)

# 输出层,无论采用什么方法输出层是一样的
preds = Dense(2, activation='softmax')(lstm1)
# 定义模型
model = Model(sequence_input, preds)
# 训练模型
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['acc'])

model.fit(x_train, y_train,
          batch_size=128,
          epochs=5,
          validation_data=(x_test, y_test))
Train on 18995 samples, validate on 2110 samples
Epoch 1/5
18995/18995 [==============================] - 245s 13ms/step - loss: 0.5133 - acc: 0.7666 - val_loss: 0.2950 - val_acc: 0.8900
Epoch 2/5
18995/18995 [==============================] - 253s 13ms/step - loss: 0.2791 - acc: 0.9066 - val_loss: 0.2369 - val_acc: 0.9171
Epoch 3/5
18995/18995 [==============================] - 256s 13ms/step - loss: 0.2004 - acc: 0.9360 - val_loss: 0.2828 - val_acc: 0.9005
Epoch 4/5
18995/18995 [==============================] - 260s 14ms/step - loss: 0.1482 - acc: 0.9561 - val_loss: 0.2738 - val_acc: 0.9133
Epoch 5/5
18995/18995 [==============================] - 268s 14ms/step - loss: 0.1171 - acc: 0.9673 - val_loss: 0.2828 - val_acc: 0.9109
# 预测
def predict(text):
    # 对句子分词
    cw = list(jieba.cut(text)) 
    word_id = []
    # 把词转换为编号
    for word in cw:
        try:
            temp = dict_text[word]
            word_id.append(temp)
        except:
            word_id.append(0)
    word_id = np.array(word_id)
    word_id = word_id[np.newaxis,:]
    sequences = pad_sequences(word_id, maxlen=1000)
    result = np.argmax(model.predict(sequences))
    if(result==1):
        print("positive comment")
    else:
        print("negative comment")
predict("用起来很麻烦,颜色一洗就掉,下次不会买了")
negative comment
predict("是一部优秀的作品,可以看出作者的文字功底深厚,有哲理")
positive comment

 

4 中文分词Seq2Seq

4.1 Seq2Seq模型

 

 

机器翻译、文章摘要、对话机器人、中文分词

左边的lstm输入句子,直到结束符,输出句子的特征。再将特征输入到右边的lstm,分别输出每个词,再输出结束符号。

4.2 数据处理

训练集:msr 微软亚洲研究院发布的语料库

{B:begin, M:middle, E:end, S:single},分别代表每个状态代表的是该字在词语中的位置,

B代表该字是词语中的起始字,M代表是词语中的中间字,E代表是词语中的结束字,S则代表是单字成词

“/s  人/b  们/e  常/s  说/s  生/b  活/e  是/s  一/s  部/s  教/b  科/m  书/e  ,/s  而/s
血/s 与/s 火/s 的/s 战/b 争/e 更/s 是/s 不/b 可/m 多/m 得/e 的/s 教/b 科/m 书/e
,/s 她/s 确/b 实/e 是/s 名/b 副/m 其/m 实/e 的/s ‘/s 我/s 的/s 大/b 学/e ’/s 。/s
# -*- coding:utf-8 -*-
import re
import numpy as np
import pandas as pd
# 截取10行的txt,打印查看
text = open('data/msr_train_10.txt').read()
text = text.split('\n')

print(text)
len(text)

 

# 设置参数
# 词向量长度
word_size = 128
# 设置最长的一句话为32个字
maxlen = 32
# 批次大小
batch_size = 1024

# 根据符号分句
text = u''.join(text) # 转成一个字符串,list-->string
text = re.split(u'[,。!?、]/[bems]', text) # 依据‘,/b’的格式划分成list
print(text)
len(text)

 

# 存数据
data = []
# 存标签
label = []

# 得到所有的数据和标签
def get_data(s):
    s = re.findall('(.)/(.)', s) # 返回值格式为[('人', 'b'),('们', 'e'),...]
    if s:
        s = np.array(s)
        # 返回数据和标签,0为字,1为标签
        return list(s[:,0]), list(s[:,1]) # 取所有行第0列 和 所有行第1列

# 对分别每一句话调用get_data
for s in text:
    d = get_data(s)
    if d:
        data.append(d[0]) # 一句话对应一个list,data这个list中存了很多list
        label.append(d[1])
# 定义一个dataframe存放数据和标签
d = pd.DataFrame(index=range(len(data))) # 从0开始的索引
d['data'] = data
d['label'] = label
# 提取data长度小于等于maxlen的数据
d = d[d['data'].apply(len) <= maxlen] 
# 每一行的data list用len函数计算长度,只留下小于maxlen的,多的行直接舍弃
# 重新排列index
d.index = range(len(d))
d

 

#统计所有字出现的频次
chars = [] 
for i in data:
    chars.extend(i)
print(chars)

chars = pd.Series(chars).value_counts() # value_counts返回每个字及出现的频次
chars

 

# 从1开始为所有字编号
chars[:] = range(1, len(chars)+1)
chars

 

#生成适合模型输入的格式
from keras.utils import np_utils

# 定义标签所对应的编号,x表示用来填充的字(0)
tag = pd.Series({'s':0, 'b':1, 'm':2, 'e':3, 'x':4})

# # 把中文变成编号,再补0
# d['x'] = d['data'].apply(lambda x: np.array(list(chars[x])+[0]*(maxlen-len(x))))
# # 把标签变成编号,再补0
# d['y'] = d['label'].apply(lambda x: np.array(list(map(lambda y:np_utils.to_categorical(y,5), tag[x].reshape((-1,1))))+[np.array([[0,0,0,0,1]])]*(maxlen-len(x))))

# 把中文变成编号,再补0
def data_helper(x):
    x = list(chars[x]) + [0]*(maxlen-len(x)) # chars[x]将list x的每个字转换成对应的编号
    return np.array(x)

# 把标签变成编号,再补0
def label_helper(x):
    # reshape((-1,1))作用是让x在tag中对应的值变成只有一列,再通过map传给lambda表达式,转成one-hot编码
    # map的第一个参数是函数,第二个是给函数传入的数据
    x = list(map(lambda y:np_utils.to_categorical(y,5), tag[x].values.reshape((-1,1))))
    x = x + [np.array([[0,0,0,0,1]])]*(maxlen-len(x))
    return np.array(x) 
    
d['x'] = d['data'].apply(data_helper) # data列数据是一行一行传进函数的
d['y'] = d['label'].apply(label_helper)   
print(d['data'][0])
print(d['x'][0])
print(d['label'][0])
print(d['y'][0])

 

# 设计模型
from keras.layers import Dense, Embedding, LSTM, TimeDistributed, Input, Bidirectional
from keras.models import Model
from keras.models import load_model


sequence = Input(shape=(maxlen,), dtype='int32')
# 词汇数,词向量长度(128维),输入的序列长度(32),是否忽略0值
# 词汇数=字典长度=输入数据最大下标+1
# mask_zero:确定是否将输入中的‘0’看作是应该被忽略的填充值
embedded = Embedding(len(chars)+1, word_size, input_length=maxlen, mask_zero=True)(sequence)
# 双向RNN包装器,输出每个词的64维向量表示。merge_mode
blstm = Bidirectional(LSTM(64, return_sequences=True), merge_mode='sum')(embedded)
# 该包装器可以把一个层应用到输入的每一个时间步上,即对每个词的输出都做五分类
output = TimeDistributed(Dense(5, activation='softmax'))(blstm)
# 定义模型输出输出
model = Model(inputs=sequence, outputs=output)
# 定义代价函数,优化器
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])


print(np.array(list(d['x'])).shape)
print(np.array(list(d['y'])).reshape((-1,maxlen,5)).shape) # (batch,32,5)
# 设置批次大小1024,epoch为20
model.fit(np.array(list(d['x'])), np.array(list(d['y'])).reshape((-1,maxlen,5)), batch_size=batch_size, epochs=20)
model.save('seq2seq.h5')

print("load model")
model = load_model('seq2seq_full.h5') # 加载的这个是在语料库上训练的
model.summary()

 

 

 

 4.3 维特比算法预测中文分词

import re
import numpy as np
import pandas as pd

# 设置最长的一句话为32个字
maxlen = 32
# 使用全数据
text = open('data/msr_train.txt').read()
text = text.split('\n')
# 根据符号分句
text = u''.join(text)
text = re.split(u'[,。!?、]/[bems]',text)

# 训练集数据
data = []
# 标签
label = []

# 得到所有的数据和标签
def get_data(s):
    s = re.findall('(.)/(.)',s)
    if s:
        s = np.array(s)
        return list(s[:,0]), list(s[:,1])

for s in text:
    d = get_data(s)
    if d:
        data.append(d[0])
        label.append(d[1])

d = pd.DataFrame(index=range(len(data)))
d['data'] = data
d['label'] = label
# 删除data长度大于maxlen的数据
d = d[d['data'].apply(len) <= maxlen]
d.index = range(len(d))

# 统计所有字,给每个字编号
chars = []
for i in data:
    chars.extend(i)
chars = pd.Series(chars).value_counts()
chars[:] = range(1, len(chars)+1)

上面的步骤主要是为了 1 用label的值统计概率 2 给每个字编号,后面用来处理测试数据

from keras.models import load_model

print("load model")
model = load_model('seq2seq_full.h5')
load model
# 统计每种状态转移次数
dict_label = {}
for label in d['label']: # 每一行数据
    for i in range(len(label)-1): # 每一个label
        tag = label[i] + label[i+1] # 前一个label加后一个label
        dict_label[tag] = dict_label.get(tag,0) + 1 # get(tag,0)字典中没有tag就返回0,有就把这类的计数加1
print(dict_label) # 最终统计出这几种状态转移组合出现的个数
{'sb': 412351, 'be': 949884, 'es': 378288, 'ss': 266154, 'bm': 183969, 'me': 183974, 'mm': 170711, 'eb': 528062}
# 计算状态转移总次数
sum_num = 0
for value in dict_label.values():
    sum_num = sum_num + value
sum_num
3073393
# 计算状态转移概率
p_ss = dict_label['ss']/sum_num
p_sb = dict_label['sb']/sum_num
p_bm = dict_label['bm']/sum_num
p_be = dict_label['be']/sum_num
p_mm = dict_label['mm']/sum_num
p_me = dict_label['me']/sum_num
p_es = dict_label['es']/sum_num
p_eb = dict_label['eb']/sum_num
# 维特比算法,维特比算法是一种动态规划算法用于寻找最有可能产生观测事件序列的-维特比路径

# tag = pd.Series({'s':0, 'b':1, 'm':2, 'e':3, 'x':4})
# 00 01 31等 表示每种情况在矩阵中的位置
# 1表示这种情况可能出现,0表示这种情况不可能出现
# 00 = ss = 1
# 01 = sb = 1
# 02 = sm = 0
# 03 = se = 0
# 10 = bs = 0
# 11 = bb = 0
# 12 = bm = 1
# 13 = be = 1
# 20 = ms = 0
# 21 = mb = 0
# 22 = mm = 1
# 23 = me = 1
# 30 = es = 1
# 31 = eb = 1
# 32 = em = 0
# 33 = ee = 0

# # 定义状态转移矩阵
# transfer = [[1,1,0,0],
#             [0,0,1,1],
#             [0,0,1,1],
#             [1,1,0,0]]

# 定义状态转移矩阵
transfer = [[p_ss,p_sb,0,0],
            [0,0,p_bm,p_be],
            [0,0,p_mm,p_me],
            [p_es,p_eb,0,0]]

# 根据符号断句
cuts = re.compile(u'([\da-zA-Z ]+)|[。,、?!\.\?,!]')

# 预测分词
def predict(sentence): # 传入分句后的一个子句子
    
    # 如果句子大于最大长度,只取maxlen个词
    if len(sentence) > maxlen:
        sentence = sentence[:maxlen]
    
    # 预测结果
    # 先把句子变成编号的形式,如果出现生僻字就填充0,list(chars[list(sentence)].fillna(0).astype(int))
    # 然后给句子补0直到maxlen的长度,[0]*(maxlen-len(sentence))
    # 然后进行预测,model.predict
    # 预测得到的结果只保留跟句子有效数据相同的长度,取[0]是要把得到的三维数据变成二维,[0][:len(sentence)]
    result = model.predict(np.array([list(chars[list(sentence)].fillna(0).astype(int))+[0]*(maxlen-len(sentence))]))[0][:len(sentence)]

    # 存放最终结果
    y = []
    # 存放临时概率值
    prob = []
    
    # 计算最大转移概率
    
    # 首先计算第1个字和第2个字,统计16种情况的概率
    # result[0][j]第1个词的标签概率
    # result[1][k]第2个词的标签概率
    # transfer[j][k]对应的转移概率矩阵的概率
    for j in range(4): # 前一个词5个标签值的概率
        for k in range(4): # 后一个词5个标签值的概率
            # 第1个词为标签j的概率*第2个词为标签k的概率*jk的转移概率
            prob.append(result[0][j]*result[1][k]*transfer[j][k])
    # prob一共有16个值
    # 要计算前一个词的的标签,要整除4,因为矩阵的行代表第一个词
    word1 = np.argmax(prob)//4
    # 计算后一个词的标签,要对4取余,因为矩阵的列代表第二个词
    word2 = np.argmax(prob)%4
    # 保存结果,把两个字的标签存到y中
    y.append(word1)
    y.append(word2)
    
    # 从第2个字开始
    for i in range(1,len(sentence)-1):
        # 存放临时概率值
        prob = []
        # 计算前一个字和后一个字的所有转移概率
        for j in range(4):
            # 前一个字的标签已知为word2
            prob.append(result[i][word2]*result[i+1][j]*transfer[word2][j])
        # 计算后一个字的标签
        word2 = np.argmax(prob)%4
        # 保存结果
        y.append(word2)
        
    # 每个字标签确定后,开始分词
    words = []
    for i in range(len(sentence)):
        # 如果标签为s或b(0/1),append到结果的list中
        if y[i] in [0, 1]:
            words.append(sentence[i])
        else:
        # 如果标签为m或e,在list最后一个元素中追加内容
            words[-1] += sentence[i]
    return words

# 分句
def cut_word(s): # 传入一个句子
    result = [] # 分词结果保存到result列表
    # 指针设置为0
    j = 0
    # 根据符号断句
    for i in cuts.finditer(s): # finditer返回一个迭代器,找到的所有的符号的位置
        # 对符号前的子句分词 0-i.start()
        result.extend(predict(s[j:i.start()]))
        # 把符号加入分割列表 
        result.append(s[i.start():i.end()])
        # 移动指针到符号后面,符号后面的位置作为下一句分割的起始位置
        j = i.end()
    # 对最后的部分进行分词
    result.extend(predict(s[j:]))
    return result

测试:

cut_word('基于seq2seq的中文分词器')
['基于', 'seq2seq', '的', '中文', '分词器']
cut_word('我爱北京天安门,天安门上太阳升')
['我', '爱', '北京', '天', '安', '门', ',', '天', '安', '门', '上', '太阳升']
cut_word('广义相对论是描写物质间引力相互作用的理论')
['广义', '相对论', '是', '描写', '物', '质', '间', '引力', '相互', '作用', '的', '理论']

有些词分的不好,需要扩充语料库

# predict函数中的长句子预测结果演示
model.predict(np.array([list(chars[list('今天天气很好')].fillna(0).astype(int))+[0]*(maxlen-len('今天天气很好'))]))[0]
array([[5.3695281e-04, 9.9946302e-01, 2.5521592e-08, 4.3570454e-09,
        1.4844584e-09],
       [1.9184302e-04, 7.1967753e-07, 1.0082871e-06, 9.9980646e-01,
        2.5343463e-09],
       [1.1647545e-06, 9.9999511e-01, 3.7472587e-06, 4.4786855e-11,
        1.2289153e-11],
       [4.9465511e-05, 1.5763325e-10, 6.9671660e-04, 9.9925381e-01,
        4.1510535e-11],
    ......
    [1.4285031e-01, 2.0312402e-05, 9.3301431e-05, 8.5702688e-01,
        9.1915690e-06],
       [1.4285031e-01, 2.0312402e-05, 9.3301431e-05, 8.5702688e-01,
        9.1915690e-06]], dtype=float32)

5 生成式对抗网络GAN

5.1 GAN

生成对抗网络(Generative Adversarial Networks,GAN)最早由 Ian Goodfellow 在 2014 年提出,是目前深度学习领域最具潜力的研究成果之一。它的核心思想是:同时训练两个相互协作、同时又相互竞争的深度神经网络(一个称为生成器 Generator,另一个称为判别器 Discriminator)来处理无监督学习的相关问题。

通常,我们会用下面这个例子来说明 GAN 的原理:将警察视为判别器,制造假币的犯罪分子视为生成器。一开始,犯罪分子会首先向警察展示一张假币。警察识别出该假币,并向犯罪分子反馈哪些地方是假的。接着,根据警察的反馈,犯罪分子改进工艺,制作一张更逼真的假币给警方检查。这时警方再反馈,犯罪分子再改进工艺。不断重复这一过程,直到警察识别不出真假,那么模型就训练成功了。

GAN的变体非常多,我们就以深度卷积生成对抗网络(Deep Convolutional GAN,DCGAN)为例,自动生成 MNIST 手写体数字。

判别器

判别器的作用是判断一个模型生成的图像和真实图像比,有多逼真。它的基本结构就是如下图所示的卷积神经网络(Convolutional Neural Network,CNN)。对于 MNIST 数据集来说,模型输入是一个 28x28 像素的单通道图像。输出层采用Sigmoid 函数,输出值在 0-1 之间,表示图像真实度的概率,其中 0 表示肯定是假的,1 表示肯定是真的。与典型的 CNN 结构相比,这里去掉了层之间的 max-pooling。这里每个 CNN 层都以 LeakyReLU 为激活函数。而且为了防止过拟合,层之间的 dropout 值均被设置在 0.4-0.7 之间,模型结构如下:

 

 

 ReLU激活函数极为f(x)=alpha * x for x < 0, f(x) = x for x>=0。alpha是一个小的非零数。

 

 

 

生成器

生成器的作用是合成假的图像,其基本结构如下图所示。图中,我们使用了卷积的倒数,即转置卷积(transposed convolution),从 100 维的噪声(满足 -1 至 1 之间的均匀分布)中生成了假图像,所谓的噪声就是100个-1到1之间的随机值。这里我们采用了模型前三层之间的上采样来合成更逼真的手写图像。在层与层之间,我们采用了批量归一化的方法来平稳化训练过程,就是对每一个中间层的输入做标准化处理,防止反向传播时低层网络的梯度消失,网络收敛越来越慢。以 ReLU 函数为每一层结构之后的激活函数。最后一层 Sigmoid 函数输出最后的假图像。第一层设置了 0.3-0.5 之间的 dropout 值来防止过拟合。

 

 批量正则化:

 

应用

1.图像生成
2.向量空间运算

 

 3.文本转图像

 

4.超分辨率

 

 

5.2 DCGAN

import numpy as np
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten, Reshape
from keras.layers import Conv2D, Conv2DTranspose, UpSampling2D
from keras.layers import LeakyReLU, Dropout
from keras.layers import BatchNormalization
from keras.optimizers import RMSprop
import matplotlib.pyplot as plt
class DCGAN(object):
    def __init__(self, img_rows=28, img_cols=28, channel=1):
        # 初始化图片的行列通道数
        self.img_rows = img_rows
        self.img_cols = img_cols
        self.channel = channel
        self.D = None   # discriminator 判别器
        self.G = None   # generator 生成器
        self.AM = None  # adversarial model 对抗模型
        self.DM = None  # discriminator model 判别模型

    # 判别模型
    def discriminator(self):
        if self.D:
            return self.D
        self.D = Sequential()
        # 定义通道数64
        depth = 64
        # dropout系数
        dropout = 0.4
        # 输入28*28*1
        input_shape = (self.img_rows, self.img_cols, self.channel)
        # 输出14*14*64
        self.D.add(Conv2D(depth*1, 5, strides=2, input_shape=input_shape, padding='same'))
        self.D.add(LeakyReLU(alpha=0.2))
        self.D.add(Dropout(dropout))
        # 输出7*7*128
        self.D.add(Conv2D(depth*2, 5, strides=2, padding='same'))
        self.D.add(LeakyReLU(alpha=0.2))
        self.D.add(Dropout(dropout))
        # 输出4*4*256
        self.D.add(Conv2D(depth*4, 5, strides=2, padding='same'))
        self.D.add(LeakyReLU(alpha=0.2))
        self.D.add(Dropout(dropout))
        # 输出4*4*512
        self.D.add(Conv2D(depth*8, 5, strides=1, padding='same'))
        self.D.add(LeakyReLU(alpha=0.2))
        self.D.add(Dropout(dropout))

        # 全连接层
        self.D.add(Flatten())
        self.D.add(Dense(1))
        self.D.add(Activation('sigmoid'))
        self.D.summary()
        return self.D

    # 生成模型
    def generator(self):
        if self.G:
            return self.G
        self.G = Sequential()
        # dropout系数
        dropout = 0.4
        # 通道数256
        depth = 64*4
        # 初始平面大小设置
        dim = 7
        # 全连接层,100个的随机噪声数据,7*7*256个神经网络
        self.G.add(Dense(dim*dim*depth, input_dim=100))
        self.G.add(BatchNormalization(momentum=0.9))
        self.G.add(Activation('relu'))
        # 把1维的向量变成3维数据(7,7,256)
        self.G.add(Reshape((dim, dim, depth)))
        self.G.add(Dropout(dropout))


        # UpSampling2D用法和 MaxPooling2D 基本相反,比如:UpSampling2D(size=(2, 2))
        # 就相当于将输入图片的长宽各拉伸一倍,整个图片被放大了
        # 上采样,采样后得到数据格式(14,14,256)
        self.G.add(UpSampling2D()) 
        # 转置卷积,得到数据格式(14,14,128) 
        self.G.add(Conv2DTranspose(int(depth/2), 5, padding='same')) # 128个
        self.G.add(BatchNormalization(momentum=0.9))
        self.G.add(Activation('relu'))

        # 上采样,采样后得到数据格式(28,28,128)
        self.G.add(UpSampling2D()) 
        # 转置卷积,得到数据格式(28,28,64) 
        self.G.add(Conv2DTranspose(int(depth/4), 5, padding='same')) # 64个
        self.G.add(BatchNormalization(momentum=0.9))
        self.G.add(Activation('relu'))

        # 转置卷积,得到数据格式(28,28,32) 
        self.G.add(Conv2DTranspose(int(depth/8), 5, padding='same')) # 32个
        self.G.add(BatchNormalization(momentum=0.9))
        self.G.add(Activation('relu'))

        # 转置卷积,得到数据格式(28,28,1) 
        self.G.add(Conv2DTranspose(1, 5, padding='same'))
        self.G.add(Activation('sigmoid'))
        self.G.summary()
        return self.G

    # 定义判别模型
    def discriminator_model(self):
        # 如果存在判别模型,直接返回
        if self.DM:
            return self.DM
        # 定义优化器
        optimizer = RMSprop(lr=0.0002, decay=6e-8)
        # 构建模型
        self.DM = Sequential()
        self.DM.add(self.discriminator())
        # binary_crossentropy对应激活函数sigmoid,categorical_crossentropy对应激活函数softmax
        self.DM.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
        return self.DM

    # 定义对抗模型
    def adversarial_model(self):
        # 检查是否存在对抗模型
        if self.AM:
            return self.AM
        # 定义优化器
        optimizer = RMSprop(lr=0.0001, decay=3e-8)
        # 构建模型
        self.AM = Sequential()
        # 生成器
        self.AM.add(self.generator())
        # 判别器
        self.AM.add(self.discriminator())
        self.AM.compile(loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'])
        return self.AM
class MNIST_DCGAN(object):
    def __init__(self):
        # 图片的行数
        self.img_rows = 28
        # 图片的列数
        self.img_cols = 28
        # 图片的通道数
        self.channel = 1

        # 载入数据
        (x_train,y_train),(x_test,y_test) = mnist.load_data()
        # (60000,28,28)
        self.x_train = x_train/255.0
        # 改变数据格式(样本数samples, rows, cols, channel),即(60000,28,28,1)
        self.x_train = self.x_train.reshape(-1, self.img_rows, self.img_cols, 1).astype(np.float32)

        # 实例化DCGAN类
        self.DCGAN = DCGAN()
        # 定义判别器模型
        self.discriminator =  self.DCGAN.discriminator_model()
        # 定义对抗模型
        self.adversarial = self.DCGAN.adversarial_model()
        # 定义生成器
        self.generator = self.DCGAN.generator()

    # 训练模型
    def train(self, train_steps=2000, batch_size=256, save_interval=0):
        noise_input = None
        if save_interval>0:
            # 生成16个100维的噪声数据
            noise_input = np.random.uniform(-1.0, 1.0, size=[16, 100])
        for i in range(train_steps): # 训练包括两个部分
            # 训练判别器,提升判别能力
            # 随机得到一个batch的图片数据
            images_train = self.x_train[np.random.randint(0, self.x_train.shape[0], size=batch_size), :, :, :]
            # 随机生成一个batch的噪声数据
            noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
            # 使用噪声数据放进生成器,生成伪造的图片数据
            images_fake = self.generator.predict(noise)
            # 合并一个batch的真实图片和一个batch的伪造图片
            x = np.concatenate((images_train, images_fake))
            # 定义标签,真实数据的标签为1,伪造数据的标签为0
            y = np.ones([2*batch_size, 1])
            y[batch_size:, :] = 0
            # 把数据放到判别器中进行判断
            d_loss = self.discriminator.train_on_batch(x, y)
            # 这部分的作用就是通过一批真的数据和一批假的数据训练判别器
        
            # 训练对抗模型,提升生成器的造假能力
            # 所有图片标签都定义为1
            y = np.ones([batch_size, 1])
            # 生成一个batch的噪声数据
            noise = np.random.uniform(-1.0, 1.0, size=[batch_size, 100])
            # 训练对抗模型:先由生成器产生图片,然后判别器优化,直到接近1
            a_loss = self.adversarial.train_on_batch(noise, y)
            
            # 打印判别器的loss和准确率,以及对抗模型的loss和准确率
            log_mesg = "%d: [D loss: %f, acc: %f]" % (i, d_loss[0], d_loss[1])
            log_mesg = "%s  [A loss: %f, acc: %f]" % (log_mesg, a_loss[0], a_loss[1])
            print(log_mesg)
            # 如果需要保存图片
            if save_interval>0:
                # 每save_interval次保存一次
                if (i+1)%save_interval==0:
                    # shape[0]16个图 shape[1]图像高度 shape[2]宽度 shape[3]通道数
                    self.plot_images(save2file=True, samples=noise_input.shape[0], noise=noise_input, step=(i+1))

    # 保存图片
    def plot_images(self, save2file=False, fake=True, samples=16, noise=None, step=0):
        # fake=true,需要保存的图片是伪造的图片
        filename = 'mnist.png'
        if fake:
            if noise is None: # 没有噪声先生成噪声
                noise = np.random.uniform(-1.0, 1.0, size=[samples, 100])
            else: # 有噪声重新起个图片名
                filename = "mnist_%d.png" % step
            # 用噪声生成伪造的图片数据
            images = self.generator.predict(noise)
        else:
            # 获得真实图片数据
            i = np.random.randint(0, self.x_train.shape[0], samples) # 取出真实图片索引
            images = self.x_train[i, :, :, :] # 取真实图片数据

        # 设置总图片大小
        plt.figure(figsize=(10,10))
        # 生成16张图片
        for i in range(images.shape[0]):
            plt.subplot(4, 4, i+1)
            # 每次获取一个张图片数据
            image = images[i, :, :, :]
            # 把image变成2维的图片
            image = np.reshape(image, [self.img_rows, self.img_cols])
            # 设置显示灰度图片
            plt.imshow(image, cmap='gray')
            # 不显示坐标轴
            plt.axis('off')
        # 保存图片
        if save2file: # 判断是否保存图片
            plt.savefig(filename)
            plt.close('all')
        # 不保存的话就显示图片
        else:
            plt.show()
# 实例化网络的类
mnist_dcgan = MNIST_DCGAN()
# 训练模型,训练1000轮,每批256,张,每500次保存一张
mnist_dcgan.train(train_steps=10000, batch_size=256, save_interval=500)

刚训练的几十轮,判别器的准确率逐步提高,可以达到90多。
然而随着训练次数增多,判别器判别不出真假了,说明生成的图片已经达到了以假乱真的效果。
生成器刚开始的准确率都是0,说明生成的噪声都是假数据。随着轮数增加不断提升,说明有部分数据被认为是真的。

mnist_dcgan.plot_images(fake=True)

 

mnist_dcgan.plot_images(fake=False)

 

mnist_dcgan.generator.save('generator.h5')
mnist_dcgan.discriminator.save('discriminator.h5')
mnist_dcgan.adversarial.save('adversarial.h5')

 

推荐阅读