Tensorflow2.X入门指南

概述

  本文是一篇较为基础的深度学习框架Tensorflow2.x入门文章,在开始之前先安装Tensorflow,当然如果你已安装,可以直接跳过。

!pip install tensorflow

  Tensorflow有CPU和GPU两个版本,GPU在深度学习中的重要性不言而喻,上面的命令默认安装CPU版本。如准备安装GPU版本,需先配置CUDA 和 cuDNN,具体内容在此不做展开,有兴趣的读者可以参考我的另一篇博文Ubuntu深度学习环境搭建。安装完成后,使用下面命令检查是否正确安装Tensorflow并被配置CUDA 和 cuDNN环境。

import tensorflow as tf
tf.config.experimental.list_physical_devices('GPU')

  由于我的主机只有一张RTX2070,所以只输出了一个GPU设备,并且设备编号为GPU:0

[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]

Part 1: TensorFlow 基础

张量

  张量是对数组和矩阵的一种扩展,张量的严格定义是利用线性映射来描述的,与矢量相类似,定义由若干坐标系改变时满足一定坐标转化关系的有序数组成的集合为张量。 从几何角度讲, 它是一个真正的几何量,也就是说,它是一个不随参照系的坐标变换(其实就是基向量变化)而变化的东西。最后结果就是基向量与对应基向量上的分量的组合(也就是张量)保持不变,比如一阶张量(向量)T可表示为T = xi + yj。(i,j被称为向量空间的基向量,基(basis)(也称为基底)是描述、刻画向量空间的基本工具。向量空间的基是它的一个特殊的子集,基的元素称为基向量。向量空间中任意一个元素,都可以唯一地表示成基向量的线性组合。如果基中元素个数有限,就称向量空间为有限维向量空间,将元素的个数称作向量空间的维数。)由于基向量的特性,张量可以表示非常丰富的物理量。

  简而言之张量是对数组和矩阵的一种扩展,多维数组就是张量在编程中的具体实现,所以在实际编程中等同于多维数组进行理解即可。其中零阶张量是标量(常数),一阶张量是向量,二阶张量是矩阵。

维基百科:张量

知乎:什么是张量?


生成一个张量常数(constant tensor):

tf.constant(value, dtype=None, shape=None, name='Const', verify_shape=False)

value为常量值或者列表,dtype为类型,shape为张量形状,name为名称、verify_shape为用于验证值的形状,默认False。

需要注意的是,张量常数类似于C++的const常量,一旦定义就不能再改变取值。

x = tf.constant([[5, 2], [1, 3]])
print(x)
tf.Tensor(
[[5 2]
 [1 3]], shape=(2, 2), dtype=int32)

通过使用张量的.numpy()属性取得张量的值

x.numpy()
array([[5, 2],
       [1, 3]], dtype=int32)

张量和Numpy的数组很相似,都具有.dtype,.shape属性

print('dtype:', x.dtype)
print('shape:', x.shape)
dtype: <dtype: 'int32'>
shape: (2, 2)

可以使用tf.ones and tf.zeros创建常量值张量 (类似于np.ones and np.zeros):

  • tf.zeros(shape, dtype=tf.float32, name=None)

    创建一个所有元素都设置为零的张量。shape为张量形状,dtype为类型,name为名称。

  • tf.zeros_like(tensor, dtype=None, name=None, optimize=True)

    给定一个张量(tensor),该操作返回与给定的张量相同类型和形状的张量,该返回张量的所有元素会被设置为零。或者使用dtype指定返回张量的新类型。tensor为给定的张量,dtype为类型,name为名称,optimize为优化项,如果为true,则尝试静态确定“张量”的形状并将其编码为常量。

  • tf.onestf.ones_like的用法和上面一致

print(tf.ones(shape=(2, 1)))
print(tf.zeros(shape=(2, 1)))
print(tf.zeros_like(x).shape)
tf.Tensor(
[[1.]
 [1.]], shape=(2, 1), dtype=float32)
tf.Tensor(
[[0.]
 [0.]], shape=(2, 1), dtype=float32)
(2, 2)
  • f.fill(dims, value, name=None)

    tf.zeros()tf.ones()是以0和1填充张量,而tf.fill()则可以指定要填充的值。参数dims表示输出张量的形状。value为要填充的值。name为名称。

tf.fill([3,3],10)
<tf.Tensor: shape=(3, 3), dtype=int32, numpy=
array([[10, 10, 10],
       [10, 10, 10],
       [10, 10, 10]], dtype=int32)>
  • tf.linspace(start, stop, num, name=None)

    产生一个等差数列一维向量,初始值是start、结束值是stop,个数是num。这个数列每次的增量是(stop - start)/(num-1)。
    在使用时发现,如果start和stop为整数时,会报错Could not find valid device for node,暂时不知道原因。

tf.linspace(1.0,10.0,10)
<tf.Tensor: shape=(10,), dtype=float32, numpy=array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.], dtype=float32)>
  • tf.range(start, limit, delta=1, dtype=None, name='range')

    产生一个等差数列的一维向量,初始值是start,增量是delta,结束值小于limit。start是初始值,如果不指定,默认是0;delta是增量,默认是1。需要注意的是,生成的整数集合是一个左开右闭区间,即不包括右端点,和Python的range函数相同。

print(tf.range(10))
print(tf.range(1,10))
tf.Tensor([0 1 2 3 4 5 6 7 8 9], shape=(10,), dtype=int32)
tf.Tensor([1 2 3 4 5 6 7 8 9], shape=(9,), dtype=int32)

随机张量常数

  • tf.random.normal(shape, mean=0.0, stddev=1.0, dtype=tf.dtypes.float32, seed=None, name=None)

    随机生成符合正态分布的张量常数

tf.random.normal(shape=(2, 2), mean=0., stddev=1.)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-1.0821191 ,  0.22778863],
       [ 1.0631582 ,  0.00430578]], dtype=float32)>
  • tf.random.uniform(shape, minval=0, maxval=None, dtype=tf.dtypes.float32, seed=None, name=None)

    随机生成符合均匀分布的张量常数,minval要生成随机数的下限,maxval要生成随机数的上限

tf.random.uniform(shape=(2, 2), minval=0, maxval=10, dtype='int32')
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[3, 9],
       [5, 1]], dtype=int32)>

变量

  变量是一种特殊的张量,用于存储动态变化的张量(比如神经网络中的权重和偏置) ,Tensorlfow使用tf.Variable()创建变量,如果在tf.device作用域内声明,则变量将被存储在该设备上;否则,变量将被存储在与其dtype兼容的“最快”设备上(这意味着大多数变量将自动放置在GPU上)。例如,以下代码片段创建一个名为的变量 v并将其放置在第二个GPU设备上:

with tf.device("/device:GPU:1"):
    v = tf.Variable(tf.zeros([10, 10]))
initial_value = tf.random.normal(shape=(2, 2))
a = tf.Variable(initial_value)
print(a)
with tf.device('/device:CPU:1'):
    b = tf.Variable(tf.zeros([10,10]))
<tf.Variable 'Variable:0' shape=(2, 2) dtype=float32, numpy=
array([[ 0.7042782 , -0.53633606],
       [ 1.292148  , -0.3515567 ]], dtype=float32)>
b
<tf.Variable 'Variable:0' shape=(10, 10) dtype=float32, numpy=
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., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)>

  使用 .assign(value), .assign_add(increment),.assign_sub(decrement)方法更新变量的值;

  • tensor.assign(values)

    使用values更新tensor的值

  • tensor.assign_add(increment)

    使用tensor+increment更新tensor的值

  • tensor.assign_sub(decrement)

    使用tensor-decrement更新tensor的值

new_value = tf.random.normal(shape=(2, 2))
a.assign(new_value)
for i in range(2):
  for j in range(2):
    assert a[i, j] == new_value[i, j]
added_value = tf.random.normal(shape=(2, 2))
a.assign_add(added_value)
for i in range(2):
  for j in range(2):
    assert a[i, j] == new_value[i, j] + added_value[i, j]

数学运算

Tensorflow的张量支持常规的数学运算,四则运算,开方,求幂等。

  • tf.multiply(tensor1,tensor2)

    计算两个张量对应元素各自相乘

  • tf.matmul(tensor1,tensor2)

    实现两个张量之间的矩阵乘法

a = tf.random.normal(shape=(2, 2))
b = tf.random.normal(shape=(2, 2))

c = a + b
d = tf.square(c)
e = tf.exp(d)

使用GradientTape实现自动微分

  Tensorflow使用tf.GradientTape()实现自动微分:

  • tf.GradientTape(persistent=False,watch_accessed_variables=True)

    persistent: 布尔值,用来指定新创建的gradient tape是否是可持续性的。默认是False,意味着只能调用gradient()函数一次,即只对导数。

    watch_accessed_variables: 布尔值,表明这个gradien tap是不是会自动追踪任何能被训练(trainable)的变量。默认是True。要是为False的话,意味着你需要手动去指定你想追踪的那些变量。

  • tape.watch(tensor)

    确保tensor被tape追踪,以记录在上下文环境中对该张量的操作

  • gradient(target,sources,output_gradients=None,unconnected_gradients=tf.UnconnectedGradients.NONE)

    根据tape上面的上下文来计算某个或者某些tensor的梯度

    target: 被微分的Tensor或者Tensor列表,你可以理解为经过某个函数之后的值

    sources: Tensors 或者Variables列表(当然可以只有一个值),可以理解为多元函数的变量集合,如二元函数f(x,y)分别对x,y求偏导,代码如下:

    with tf.GradientTape(persistent=False,watch_accessed_variables=True) as tape:
        f_x,f_y = tape.gradient(f,[x,y])
    

    return:一个列表,存储各个变量的梯度值,和source中的变量列表一一对应,表明这个变量的梯度。

在上下文内部的持久性磁带上调用GradientTape.gradient()效率要比在上下文外部进行调用的效率低得多(这会导致求导操作记录在磁带上,从而导致CPU和内存使用量增加)。如果您实际上要跟踪导数以计算高阶导数,则仅能在上下文内调用GradientTape.gradient()

a = tf.random.normal(shape=(2, 2))
b = tf.random.normal(shape=(2, 2))

with tf.GradientTape(persistent=True,watch_accessed_variables=True) as tape:
    # 开始记录对张量a的左右操作
    tape.watch(a)
    # 使用张量定义函数
    f = tf.sqrt(tf.square(a) + tf.square(b))  
    # 求f关于a,b的偏导数
    f_a = tape.gradient(f,a)
    f_a1 = tape.gradient(f_a,a)
print(f_a1)
WARNING:tensorflow:Calling GradientTape.gradient on a persistent tape inside its context is significantly less efficient than calling it outside the context (it causes the gradient ops to be recorded on the tape, leading to increased CPU and memory usage). Only call GradientTape.gradient inside the context if you actually want to trace the gradient in order to compute higher order derivatives.
tf.Tensor(
[[6.9585860e-02 3.9474440e-01]
 [1.1950731e-04 1.6646397e-01]], shape=(2, 2), dtype=float32)

一般会默认对变量进行追踪,所以不需要显式指定

a = tf.Variable(a)

with tf.GradientTape() as tape:
  c = tf.sqrt(tf.square(a) + tf.square(b))
  dc_da = tape.gradient(c, a)
  print(dc_da)
tf.Tensor(
[[ 0.7646002   0.48451895]
 [-0.30007327  0.5063112 ]], shape=(2, 2), dtype=float32)

也可以通过嵌套tf.GradientTape计算高阶导数

with tf.GradientTape() as outer_tape:
  with tf.GradientTape() as tape:
    c = tf.sqrt(tf.square(a) + tf.square(b))
    dc_da = tape.gradient(c, a)
  dc = outer_tape.gradient(dc_da, a)
  print(dc)
tf.Tensor(
[[0.18161985 0.72908604]
 [0.88818395 0.3431898 ]], shape=(2, 2), dtype=float32)

示例一

  接下来基于一个构造的线性可分的二分类数据集,使用Tensorflow训练单层前馈神经网络来作为这个数据集的二分类器。

class Perceptron(object):

    def __init__(self):

        self.input_dim = 2
        self.output_dim = 1
        self.learning_rate = 0.01
        self.w = tf.Variable(tf.random.uniform(shape=(self.input_dim, self.output_dim)))
        self.b = tf.Variable(tf.zeros(shape=(self.output_dim,)))

    def compute_predictions(self,features):
        return tf.matmul(features, self.w) + self.b

    def compute_loss(self,labels, predictions):
        return tf.reduce_mean(tf.square(labels - predictions))

    def train_on_batch(self,x, y):

        with tf.GradientTape(persistent=False,watch_accessed_variables=True) as tape:  
            predictions = self.compute_predictions(x)
            loss = self.compute_loss(y, predictions)
            dloss_dw, dloss_db = tape.gradient(loss, [self.w, self.b])
        self.w.assign_sub(self.learning_rate * dloss_dw)
        self.b.assign_sub(self.learning_rate * dloss_db)
        return loss

生成二分类数据集用于训练分类器

import numpy as np
import random
import matplotlib.pyplot as plt
%matplotlib inline

num_samples = 10000
# 负样本
negative_samples = np.random.multivariate_normal(
    mean=[0, 3], cov=[[1, 0.5],[0.5, 1]], size=num_samples)
# 正样本
positive_samples = np.random.multivariate_normal(
    mean=[3, 0], cov=[[1, 0.5],[0.5, 1]], size=num_samples)
# 沿着竖直方向将数组堆叠起来。将正样本和负样本进行拼接得到数据集
features = np.vstack((negative_samples, positive_samples)).astype(np.float32)
# 生成正负样本对应的标签
labels = np.vstack((np.zeros((num_samples, 1), dtype='float32'),
                    np.ones((num_samples, 1), dtype='float32')))

plt.scatter(features[:, 0], features[:, 1], c=labels[:, 0])
png

接下来,通过将数据逐次迭代训练感知器

# 生成随机数据序列
indices = np.random.permutation(len(features))
features = features[indices]
labels = labels[indices]
percep = Perceptron()
# 使用tf.data.Dataset对象来批量迭代数据进行训练
dataset = tf.data.Dataset.from_tensor_slices((features, labels))
dataset = dataset.shuffle(buffer_size=1024).batch(256)

for epoch in range(10):
  for step, (x, y) in enumerate(dataset):
    loss = percep.train_on_batch(x, y)
  print('Epoch %d: last batch loss = %.4f' % (epoch, float(loss)))
Epoch 0: last batch loss = 0.0634
Epoch 1: last batch loss = 0.0325
Epoch 2: last batch loss = 0.0262
Epoch 3: last batch loss = 0.0275
Epoch 4: last batch loss = 0.0211
Epoch 5: last batch loss = 0.0216
Epoch 6: last batch loss = 0.0264
Epoch 7: last batch loss = 0.0172
Epoch 8: last batch loss = 0.0256
Epoch 9: last batch loss = 0.0250
predictions = percep.compute_predictions(features)
plt.scatter(features[:, 0], features[:, 1], c=predictions[:, 0] > 0.5)
png

使用tf.function为网络训练加速

  Tensorflow 2.0默认开启eager模式,即动态图模式,可以立即得到运行结果,便于调试。但是由于动态图无法进行图优化,其计算速度比静态图慢,为此官方推出静态图转换器tf.function可以显著提升网络的训练速度。

import time

t_start = time.time()
for epoch in range(20):
  for step, (x, y) in enumerate(dataset):
    loss = percep.train_on_batch(x, y)
t_end = time.time() - t_start
print('训练耗时: %.3f s' % (t_end / 20,))

训练耗时: 0.126 s

通过在函数上添加tf.function装饰器,将训练函数编译成静态图形。

@tf.function
def train_on_batch(x, y):
  with tf.GradientTape() as tape:
    predictions = percep.compute_predictions(x)
    loss = percep.compute_loss(y, predictions)
    dloss_dw, dloss_db = tape.gradient(loss, [percep.w, percep.b])
  percep.w.assign_sub(percep.learning_rate * dloss_dw)
  percep.b.assign_sub(percep.learning_rate * dloss_db)
  return loss

转为静态图之后的训练速度

t_start = time.time()
for epoch in range(20):
  for step, (x, y) in enumerate(dataset):
    loss = train_on_batch(x, y)
t_end = time.time() - t_start
print('训练耗时: %.3f s' % (t_end / 20,))
训练耗时: 0.069 s

  可以看到,对于这个简单的模型使用静态图转换器后,训练时间减少了将近47%的时间,而对于更大、更复杂的模型静态图转换器的效果越是显著。动态图模式在调试和打印输出是很方便的,但是在实际部署和训练时,静态图是更好的选择。

Part 2: Keras API

  Keras是基于Python的深度学习库,目前已被纳入Tensorflow体系,成为Tensorflow的高级API,由于其易于扩展的高度模块化设计,能够以最快的速度实现idea,备受欢迎。

Layer

  Layer是Keras最基础也是最重要的类,一个Layer封装了一个状态(权重)和一些计算(在call方法中定义),Keras基于Layer来搭建更复杂的模型,接下我们来实现一个简易的Layer类。

from tensorflow.keras.layers import Layer
import tensorflow as tf

class Linear(Layer):
  """
  构建一个简易的Layer类,主要实现y = w.x + b的计算
  """
  def __init__(self, units=32, input_dim=32):
      super(Linear, self).__init__()
      w_init = tf.random_normal_initializer()
      self.w = tf.Variable(
          initial_value=w_init(shape=(input_dim, units), dtype='float32'),
          trainable=True)
      b_init = tf.zeros_initializer()
      self.b = tf.Variable(
          initial_value=b_init(shape=(units,), dtype='float32'),
          trainable=True)

  def call(self, inputs):
      return tf.matmul(inputs, self.w) + self.b

linear_layer = Linear(4, 2)
y = linear_layer(tf.ones((2, 2)))
assert y.shape == (2, 4)

Layer 类的weights负责追踪记录权重和偏置的变化

assert linear_layer.weights == [linear_layer.w, linear_layer.b]

先回顾下Linear在定义权重时是怎么进行初始化的:

w_init = tf.random_normal_initializer()
self.w = tf.Variable(initial_value=w_init(shape=shape, dtype='float32'))

先创建了一个正态分布初始器,然后再用于初始化权重变量。

采用add_weight的方式来进行初始化会更简洁一些:

self.w = self.add_weight(shape=shape, initializer='random_normal')

另外,在上面的写法中,我们在类的初始函数__init__中创建并初始化权重和偏置,为此我们需要传入网络的输入和输出维度。最好的做法是使用一个单独的build方法来创建权重和偏置,只有在调用该方法时才创建并初始化权重和偏置,并且无需传入网络的输入维度。

class Linear(Layer):
  """y = w.x + b"""
  def __init__(self, units=32):
      super(Linear, self).__init__()
      self.units = units

  def build(self, input_shape):
      self.w = self.add_weight(shape=(input_shape[-1], self.units),
                               initializer='random_normal',
                               trainable=True)
      self.b = self.add_weight(shape=(self.units,),
                               initializer='random_normal',
                               trainable=True)

  def call(self, inputs):
      return tf.matmul(inputs, self.w) + self.b


# 初始化层时只需传入网络的输出维度即可
linear_layer = Linear(4)

# This will also call `build(input_shape)` and create the weights.
y = linear_layer(tf.ones((2, 2)))
assert len(linear_layer.weights) == 2

Trainable

网络的权重在被创建时可以指定Trainable为True,即定义该变量为可训练的参数,而对于一些记录计算步骤的'步骤'变量显然是不可训练的,tf.GradientTape默认追踪可训练变量。详情如下,创建一个不能被训练的权重变量。

tf.reduce_sum(input_tensor, axis=None, keepdims=False, name=None)

根据轴(axis)计算张量指定维度的和,需要注意的是,其计算规则与numpy的sum正好相反。axis=0时,按列求和,axis=1时,按行求和,如下:

x = tf.constant([[1, 1, 1], [1, 1, 1]])
tf.reduce_sum(x)  # 6
tf.reduce_sum(x, axis=0)  # [2, 2, 2]
tf.reduce_sum(x, axis=1)  # [3, 3]
tf.reduce_sum(x, axis=1, keepdims=True)  # [[3], [3]]
tf.reduce_sum(x, [0, 1])  # 6
 from tensorflow.keras.layers import Layer

class ComputeSum(Layer):
  """Returns the sum of the inputs."""

  def __init__(self, input_dim):
      super(ComputeSum, self).__init__()
      # Create a non-trainable weight.
      self.total = tf.Variable(initial_value=tf.zeros((input_dim,)),
                               trainable=False)

  def call(self, inputs):
      self.total.assign_add(tf.reduce_sum(inputs, axis=0))
      return self.total  

my_sum = ComputeSum(2)
x = tf.ones((2, 2))

y = my_sum(x)
print(y.numpy())  # [2. 2.]

y = my_sum(x)
print(y.numpy())  # [4. 4.]

assert my_sum.weights == [my_sum.total]
assert my_sum.non_trainable_weights == [my_sum.total]
assert my_sum.trainable_weights == []
[2. 2.]
[4. 4.]

创建深度网络

  在上文中,我们已经实现了单层神经网络,接下来通过递归嵌套单层网络来创建更大的计算块,每一层将跟踪其子层的权重(可训练和不可训练)。接下来使用之前定义的Linear类和其build方法来递归创建更复杂、更深的网络。需要注意的是,目前比较主流的观点是,更深的神经网络(即隐藏层更多)往往比更宽的神经网络有着更好的表征能力并且也更容易训练。

class MLP(Layer):
    """Simple stack of Linear layers."""

    def __init__(self):
        super(MLP, self).__init__()
        self.linear_1 = Linear(32)
        self.linear_2 = Linear(32)
        self.linear_3 = Linear(10)

    def call(self, inputs):
        x = self.linear_1(inputs)
        x = tf.nn.relu(x)
        x = self.linear_2(x)
        x = tf.nn.relu(x)
        return self.linear_3(x)

mlp = MLP()

# 初始化网络
y = mlp(tf.ones(shape=(3, 32)))

# 权重将被递归追踪
assert len(mlp.weights) == 6

Keras内置层

Keras提供了广泛的内置层,不需要对模型进行更细粒度的控制或者实现新颖的结构,一般不需要自己进行实现。keras已实现的层如下:

  • Convolution layers
  • Transposed convolutions
  • Separateable convolutions
  • Average and max pooling
  • Global average and max pooling
  • LSTM, GRU (with built-in cuDNN acceleration)
  • BatchNormalization
  • Dropout
  • Attention
  • ConvLSTM2D
  • etc.

更多详细内容请参考官方文档
Keras内建层

温馨提示

  Keras的内置层中有一些较为特殊,比如BatchNormalization层和Dropout层,在训练和推理期间具有不同的行为。对于此类层,标准做法是在call方法中公开训练(布尔)参数。 通过在调用中公开此参数,可以启用内置的训练和评估循环(例如,拟合)以在训练和推理中正确使用该图层。


tf.keras.layers.Dropout(rate, noise_shape=None, seed=None, **kwargs)

每次训练时随机忽略一部分神经元,这些神经元被静默了。换句话讲,这些神经元在正向传播时对下游的启动影响被忽略,反向传播时也不会更新权重,使得网络对某个神经元的权重变化更不敏感,增加泛化能力,减少过拟合,其中rate为被静默的比例。更详细内容请参考Srivastava等大牛在2014年的论文《Dropout: A Simple Way to Prevent Neural Networks from Overfitting》


tf.keras.layers.BatchNormalization(
    axis=-1,
    momentum=0.99,
    epsilon=0.001,
    center=True,
    scale=True,
    beta_initializer='zeros',
    gamma_initializer='ones',
    moving_mean_initializer='zeros',
    moving_variance_initializer='ones',
    beta_regularizer=None,
    gamma_regularizer=None,
    beta_constraint=None,
    gamma_constraint=None,
    renorm=False,
    renorm_clipping=None,
    renorm_momentum=0.99,
    fused=None,
    trainable=True,
    virtual_batch_size=None,
    adjustment=None,
    name=None,
    **kwargs,
)

批量标准化层 (Ioffe and Szegedy, 2014)
在每一个批次的数据中标准化前一层的激活项,即,应用一个维持激活项平均值接近0,标准差接近1的转换。

  • Paramas:

    axis: 整数,需要标准化的轴 (通常是特征轴)。 例如,在 data_format="channels_first"的 Conv2D 层之后, 在 BatchNormalization中设置 axis=1。

    momentum: 移动均值和移动方差的动量。

    epsilon: 增加到方差的小的浮点数,以避免除以零。

    center: 如果为 True,把 beta 的偏移量加到标准化的张量上。 如果为 False, beta 被忽略。

    scale: 如果为 True,乘以 gamma。 如果为 False,gamma 不使用。 当下一层为线性层(或者例如 nn.relu), 这可以被禁用,因为缩放将由下一层完成。

    beta_initializer: beta 权重的初始化方法。

    gamma_initializer: gamma 权重的初始化方法。

    moving_mean_initializer: 移动均值的初始化方法。

    moving_variance_initializer: 移动方差的初始化方法。

    beta_regularizer: 可选的 beta 权重的正则化方法。

    gamma_regularizer: 可选的 gamma 权重的正则化方法。

    beta_constraint: 可选的 beta 权重的约束方法。

    gamma_constraint: 可选的 gamma 权重的约束方法。

批标准化技术能显著提升神经网络的训练速度和泛化能力,是神经网络中比较重要的优化技术之一。想要进一步了解其原理的读者,请参考原论文
Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift

from tensorflow.keras.layers import Layer

class Dropout(Layer):
  
  def __init__(self, rate):
    super(Dropout, self).__init__()
    self.rate = rate

  def call(self, inputs, training=None):
    if training:
      return tf.nn.dropout(inputs, rate=self.rate)
    return inputs

class MLPWithDropout(Layer):

  def __init__(self):
      super(MLPWithDropout, self).__init__()
      self.linear_1 = Linear(32)
      self.dropout = Dropout(0.5)
      self.linear_3 = Linear(10)

  def call(self, inputs, training=None):
      x = self.linear_1(inputs)
      x = tf.nn.relu(x)
      x = self.dropout(x, training=training)
      return self.linear_3(x)
    
mlp = MLPWithDropout()
y_train = mlp(tf.ones((2, 2)), training=True)
y_test = mlp(tf.ones((2, 2)), training=False)

函数式API

  要构建深度学习模型,不必一直使用面向对象的编程。网络层也可以使用Tensorflow的函数式API按功能进行组合,如下所示:

# 使用Input描述神经网络输入的形状和类型
inputs = tf.keras.Input(shape=(16,))


x = Linear(32)(inputs) 
x = Dropout(0.5)(x) 
outputs = Linear(10)(x)

# 通过指定输入和输出来定义功能性的"模型"。 
# 模型本身就是一个网络层。
model = tf.keras.Model(inputs, outputs)

# 在调用任何数据之前,功能模型已经具有权重。这是因为在Input中预先定义了其输入形状。
assert len(model.weights) == 4


y = model(tf.ones((2, 16)))
assert y.shape == (2, 10)

  Keras功能性API是一种创建模型的方法,该模型比tf.keras.SequentialAPI更灵活。功能性API可以处理具有非线性拓扑的模型,具有共享层的模型以及具有多个输入或输出的模型。深度学习模型通常是层的有向无环图(DAG)的主要思想,因此,功能性API是一种构建层图的方法。

  对于具有单个输入和单个输出的简单图层堆叠的模型,可以直接使用Sequential类将图层列表转换为Model。

from tensorflow.keras import Sequential

model = Sequential([Linear(32), Dropout(0.5), Linear(10)])

y = model(tf.ones((2, 16)))
assert y.shape == (2, 10)

损失函数

Keras实现了目前大部分主流的损失函数,例如BinaryCrossentropyCategoricalCrossentropyKLDivergence等。
需要特别注意的是训练多分类神经网络时一般使用交叉熵损失函数,需要注意的好是:

  • 对于二分类神经网络,一般使用BinaryCrossentropy交叉熵函数作为损失函数:
tf.keras.losses.BinaryCrossentropy(
    from_logits=False, label_smoothing=0, reduction=losses_utils.ReductionV2.AUTO,
    name='binary_crossentropy'
)
  • 对于标签为整数形式的多分类神经网络,一般使用SparseCategoricalCrossentropy交叉熵函数作为损失函数:
tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=False, reduction=losses_utils.ReductionV2.AUTO,
    name='sparse_categorical_crossentropy'
)
  • 对于标签为为One-Hot形式的多分类神经网络,一般使用CategoricalCrossentropy交叉熵函数作为损失函数:
tf.keras.losses.CategoricalCrossentropy(
    from_logits=False, label_smoothing=0, reduction=losses_utils.ReductionV2.AUTO,
    name='categorical_crossentropy'
)

Note:三个函数都有一个共同的参数from_logits,该参数表示是否将网络输出层中使用Logits函数或Softmax函数(多维形式的Logits)激活的输出映射为真实值,默认为False,将输出值映射真实值再带入损失函数计算。

使用方法如下:

bce = tf.keras.losses.BinaryCrossentropy()
y_true = [0., 0., 1., 1.]  # Targets
y_pred = [1., 1., 1., 0.]  # Predictions
loss = bce(y_true, y_pred)
print('Loss:', loss.numpy())
Loss: 11.522857

需要注意的是,模型的损失是无状态的:__call__的输出仅是输入的函数。

评价函数

Keras还实现了评价函数,例如BinaryAccuracyAUCFalsePositives等。评价函数用于评估当前训练模型的性能。当模型编译后(compile),评价函数应该作为metrics的参数来输入。与损失函数不同,评价函数是有状态的。可以使用update_state方法更新其状态,并使用result查询标量度量结果:

m = tf.keras.metrics.AUC()
m.update_state([0, 1, 1, 1], [0, 1, 0, 0])
print('第一步更新的结果:',m.result().numpy())

m.update_state([1, 1, 1, 1], [0, 1, 1, 0])
print('第二步更新的结果:', m.result().numpy())
第一步更新的结果: 0.6666667
第二步更新的结果: 0.71428573

可以使用etric.reset_states清除评价函数内部状态。

当然,如果有必要,也可以通过将Metric类子类化来轻松推出自己的指标:

  • __init__中创建状态变量
  • update_state中更新给定y_truey_pred的变量 返回结果中的度量结果
  • 清除reset_states中的状态

通过下面的例子来演示如何实现自己的评价类:

class BinaryTruePositives(tf.keras.metrics.Metric):

  def __init__(self, name='binary_true_positives', **kwargs):
    super(BinaryTruePositives, self).__init__(name=name, **kwargs)
    self.true_positives = self.add_weight(name='tp', initializer='zeros')

  def update_state(self, y_true, y_pred, sample_weight=None):
    # 将张量转为布尔类型
    y_true = tf.cast(y_true, tf.bool)
    y_pred = tf.cast(y_pred, tf.bool)

    values = tf.logical_and(tf.equal(y_true, True), tf.equal(y_pred, True))
    values = tf.cast(values, self.dtype)
    if sample_weight is not None:
      sample_weight = tf.cast(sample_weight, self.dtype)
      values = tf.multiply(values, sample_weight)
    self.true_positives.assign_add(tf.reduce_sum(values))

  def result(self):
    return self.true_positives

  def reset_states(self):
    self.true_positive.assign(0)

m = BinaryTruePositives()
m.update_state([0, 1, 1, 1], [0, 1, 0, 0])
print('第一步更新的结果:', m.result().numpy())

m.update_state([1, 1, 1, 1], [0, 1, 1, 0])
print('第二步更新的结果:', m.result().numpy())
第一步更新的结果: 1.0
第二步更新的结果: 3.0

优化器

  深度学习问题可以归结为一个最优化问题:最优化最小目标函数,通过使用各种优化器来更新和计算影响神经网络中的参数,使目标函数逼近或达到最优值,从而最小化目标函数。常见的优化器有Adadelta,Adagrad,RMSProp,Adam等,关于优化器的原理由此篇幅有限在此不做展开,有兴趣的读者,可以参考下面这篇博客。

An overview of gradient descent optimization algorithms

MNIST

  MNIST是机器学习领域中的一个经典问题。该问题解决的是把28x28像素的灰度手写数字图片识别为相应的数字,其中数字的范围从0到9。

  接下来,我们通过经典的深度学习示例MNIST手写数字识别学习如何使用各种优化器。

  首先使用keras的dataset加载Mnist数据集,该数据集有两大部分组成,一部分是70000个28x28像素的灰度图片样本(下图是其中一个样本1的手写灰度图),另一部分则是这些图片样本对应的数字标签:

Mnist

  接下来需要使用Mnist数据集训练一个神经网络,来识别手写数字。

  • 接下来先加载Mnist数据集,Keras提供的API默认将该数据划分为训练集和验证集,其样本数目分别为60000,10000,为了更好的对模型进行评估,我们将数据集重新进行划分,从60000个样本的训练集中再分出10000个样本作为测试集来评估模型的最终表现。
(x_train,y_train),(x_validation,y_validation) = tf.keras.datasets.mnist.load_data()
x_train,y_train,x_test,y_test = x_train[:50000,:,:],y_train[:50000],x_train[50000:,:,:],y_train[50000:]
import matplotlib.pyplot as plt
print(y_train[1])
_ = plt.imshow(x_train[1,:,:])
0
png
from tensorflow.keras import layers


(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
# 由于只是使用前馈神经网络进行训练,将28*28的图片展开成784的行或列向量,并进行标准化
x_train = x_train[:].reshape(60000, 784).astype('float32') / 255
# 使用tf.data.Dataset API构建输入管道
dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# shuffle创建了一个固定大小的缓存区,每次从数据集中随机抽取固定数目的样本存入缓存区
# batch 每次从缓存区中无放回的取出指定大小的数据组成一个batch,直到取出全部元素 
dataset = dataset.shuffle(buffer_size=1024).batch(64)

model = tf.keras.Sequential([
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(10)
])

# 使用交叉熵损失函数来对网络进行评估
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
# 计算训练过程中的识别准确率
accuracy = tf.keras.metrics.SparseCategoricalAccuracy()
# 使用Adam优化器
optimizer = tf.keras.optimizers.Adam()

for step, (x, y) in enumerate(dataset):
  
  with tf.GradientTape() as tape:

    # 计算前向过程
    logits = model(x)

    # 计算当前Batch的损失函数
    loss_value = loss(y, logits)
     
  # 使用损失函数计算所有可训练参数的梯度
  gradients = tape.gradient(loss_value, model.trainable_weights)
  
  # 使用计算的梯度更新所有可训练参数
  optimizer.apply_gradients(zip(gradients, model.trainable_weights))

  # 更新当前的预测精度
  accuracy.update_state(y, logits)
  
  # 训练过程
  if step % 100 == 0:
    print('Epochs:', step)
    print('Loss from last step: %.3f' % loss_value)
    print('Total running accuracy so far: %.3f' % accuracy.result())
Step: 0
Loss from last step: 2.408
Total running accuracy so far: 0.094
Step: 100
Loss from last step: 0.426
Total running accuracy so far: 0.842
Step: 200
Loss from last step: 0.511
Total running accuracy so far: 0.880
Step: 300
Loss from last step: 0.084
Total running accuracy so far: 0.898
Step: 400
Loss from last step: 0.321
Total running accuracy so far: 0.910
Step: 500
Loss from last step: 0.114
Total running accuracy so far: 0.917
Step: 600
Loss from last step: 0.152
Total running accuracy so far: 0.924
Step: 700
Loss from last step: 0.175
Total running accuracy so far: 0.928
Step: 800
Loss from last step: 0.153
Total running accuracy so far: 0.931
Step: 900
Loss from last step: 0.060
Total running accuracy so far: 0.935

使用 SparseCategoricalAccuracy 指标函数计算每一个Batch测试数据的准确率。

x_test = x_test[:].reshape(10000, 784).astype('float32') / 255
test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_dataset = test_dataset.batch(128)

accuracy.reset_states()  # This clears the internal state of the metric

for step, (x, y) in enumerate(test_dataset):
  logits = model(x)
  accuracy.update_state(y, logits)

print('Final test accuracy: %.3f' % accuracy.result())
Final test accuracy: 0.964

add_loss

  有时侯,需要在前向计算期间动态计算损失值(尤其是正则化损失)。 Keras通过add_loss方法随时计算损失值,并持续追踪损失值。 下面是一个基于L2范数的正则化损失示例:

from tensorflow.keras.layers import Layer

class ActivityRegularization(Layer):
  """Layer that creates an activity sparsity regularization loss."""
  
  def __init__(self, rate=1e-2):
    super(ActivityRegularization, self).__init__()
    self.rate = rate
  
  def call(self, inputs):
    # We use `add_loss` to create a regularization loss
    # that depends on the inputs.
    self.add_loss(self.rate * tf.reduce_sum(tf.square(inputs)))
    return inputs

可以通过任何图层或模型的.losses属性查看通过add_loss添加的损失值:

from tensorflow.keras import layers

class SparseMLP(Layer):

  def __init__(self, output_dim):
      super(SparseMLP, self).__init__()
      self.dense_1 = layers.Dense(32, activation=tf.nn.relu)
      self.regularization = ActivityRegularization(1e-2)
      self.dense_2 = layers.Dense(output_dim)

  def call(self, inputs):
      x = self.dense_1(inputs)
      x = self.regularization(x)
      return self.dense_2(x)
    

mlp = SparseMLP(1)
y = mlp(tf.ones((10, 10)))

print(mlp.losses)
# Losses correspond to the *last* forward pass.
mlp = SparseMLP(1)
mlp(tf.ones((10, 10)))
assert len(mlp.losses) == 1
mlp(tf.ones((10, 10)))
assert len(mlp.losses) == 1  # No accumulation.

# Let's demonstrate how to use these losses in a training loop.

# Prepare a dataset.
(x_train, y_train), _ = tf.keras.datasets.mnist.load_data()
dataset = tf.data.Dataset.from_tensor_slices(
    (x_train.reshape(60000, 784).astype('float32') / 255, y_train))
dataset = dataset.shuffle(buffer_size=1024).batch(64)

# A new MLP.
mlp = SparseMLP(10)

# Loss and optimizer.
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
optimizer = tf.keras.optimizers.SGD(learning_rate=0.1)

for step, (x, y) in enumerate(dataset):
  with tf.GradientTape() as tape:
    # Forward pass.
    logits = mlp(x)

    # External loss value for this batch.
    loss = loss_fn(y, logits)
    
    # Add the losses created during the forward pass.
    loss += sum(mlp.losses)
     
    # Get gradients of loss wrt the weights.
    gradients = tape.gradient(loss, mlp.trainable_weights)
  
  # Update the weights of our linear layer.
  optimizer.apply_gradients(zip(gradients, mlp.trainable_weights))
  
  # Logging.
  if step % 100 == 0:
    print('Loss at step %d: %.3f' % (step, loss))

Sequential

  诚然关注底层设计能带来更细粒度的控制,但是对于简单的模型,如果采用上面的方式来实现,显然是没有必要的。 Keras提供了相关的高级API来搭建神经网络,如Model类的子类、函数式API或顺序模型Sequential。 下面使用Keras的Sequential搭建一个MNIST分类网络:

# Prepare a dataset.
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype('float32') / 255
dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
dataset = dataset.shuffle(buffer_size=1024).batch(64)

# Instantiate a simple classification model
model = tf.keras.Sequential([
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(10)
])

# Instantiate a logistic loss function that expects integer targets.
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Instantiate an accuracy metric.
accuracy = tf.keras.metrics.SparseCategoricalAccuracy()

# Instantiate an optimizer.
optimizer = tf.keras.optimizers.Adam()

定义模型结构后,配置模型训练的优化器,损失函数和指标函数

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

开始训练网络

model.fit(dataset, epochs=3)

Note: 当使用fit时,默认情况下使用静态图执行,因此无需在模型或图层中添加任何tf.function装饰器。

x_test = x_test[:].reshape(10000, 784).astype('float32') / 255
test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_dataset = test_dataset.batch(128)

loss, acc = model.evaluate(test_dataset)
print('loss: %.3f - acc: %.3f' % (loss, acc))

可以在训练期间监视某些验证数据上的损失和指标。 另外,Keras还支持直接在Numpy数组上调用fit,因此不需要数据集转换:

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype('float32') / 255

num_val_samples = 10000
x_val = x_train[-num_val_samples:]
y_val = y_train[-num_val_samples:]
x_train = x_train[:-num_val_samples]
y_train = y_train[:-num_val_samples]

# Instantiate a simple classification model
model = tf.keras.Sequential([
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(10)
])

# Instantiate a logistic loss function that expects integer targets.
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Instantiate an accuracy metric.
accuracy = tf.keras.metrics.SparseCategoricalAccuracy()

# Instantiate an optimizer.
optimizer = tf.keras.optimizers.Adam()

model.compile(optimizer=optimizer,
              loss=loss,
              metrics=[accuracy])
model.fit(x_train, y_train,
          validation_data=(x_val, y_val),
          epochs=3,
          batch_size=64)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 0s 0us/step
Epoch 1/3
782/782 [==============================] - 4s 5ms/step - loss: 0.2434 - sparse_categorical_accuracy: 0.9291 - val_loss: 0.1209 - val_sparse_categorical_accuracy: 0.9632
Epoch 2/3
782/782 [==============================] - 3s 4ms/step - loss: 0.0947 - sparse_categorical_accuracy: 0.9704 - val_loss: 0.0841 - val_sparse_categorical_accuracy: 0.9732
Epoch 3/3
782/782 [==============================] - 3s 4ms/step - loss: 0.0609 - sparse_categorical_accuracy: 0.9806 - val_loss: 0.0982 - val_sparse_categorical_accuracy: 0.9715

Callbacks

  fit的简洁功能之一(内置了对样本加权和类别加权的支持)是可以使用callbacks.轻松地自定义训练和评估期间发生的情况。 回调是在训练过程中(例如,每个batch或epoch结束时)在不同时间点调用的对象,并执行一些操作,例如保存模型,加载检查点,停止培训等回调函数,例如ModelCheckpoint可以在训练期间的每个纪元之后保存模型,或者EarlyStopping可以在验证指标开始停止时中断训练。
还可以轻松编写自己的回调.

# Instantiate a simple classification model
model = tf.keras.Sequential([
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(256, activation=tf.nn.relu),
  layers.Dense(10)
])

# Instantiate a logistic loss function that expects integer targets.
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Instantiate an accuracy metric.
accuracy = tf.keras.metrics.SparseCategoricalAccuracy()

# Instantiate an optimizer.
optimizer = tf.keras.optimizers.Adam()

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

# Instantiate some callbacks
callbacks = [tf.keras.callbacks.EarlyStopping(),
             tf.keras.callbacks.ModelCheckpoint(filepath='my_model.keras',
                                                save_best_only=True)]

model.fit(x_train, y_train,
          validation_data=(x_val, y_val),
          epochs=30,
          batch_size=64,
          callbacks=callbacks)
Epoch 1/30
782/782 [==============================] - 4s 4ms/step - loss: 0.2391 - sparse_categorical_accuracy: 0.9284 - val_loss: 0.1330 - val_sparse_categorical_accuracy: 0.9600
Epoch 2/30
782/782 [==============================] - 3s 4ms/step - loss: 0.0951 - sparse_categorical_accuracy: 0.9708 - val_loss: 0.0795 - val_sparse_categorical_accuracy: 0.9753
Epoch 3/30
782/782 [==============================] - 3s 4ms/step - loss: 0.0627 - sparse_categorical_accuracy: 0.9804 - val_loss: 0.0864 - val_sparse_categorical_accuracy: 0.9751

HiPlot

  调参是深度学习模型训练的一个难点,最后给大家推一个模型调参使用的可视化工具。

使用KerasTuner和Hiplot进行神经网络超参数调整

import hiplot as hip
data = [{'dropout':0.1, 'lr': 0.001, 'loss': 10.0, 'optimizer': 'SGD'},
        {'dropout':0.15, 'lr': 0.01, 'loss': 3.5, 'optimizer': 'Adam'},
        {'dropout':0.3, 'lr': 0.1, 'loss': 4.5, 'optimizer': 'Adam'}]
hip.Experiment.from_iterable(data).display(store_state_key="cell1")
image
image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272