Python Machine Learning 3rd Edition by Sebastian Raschka & Vahid Mirjalili, Packt Publishing Ltd. 2019
Code Repository: https://github.com/rasbt/python-machine-learning-book-3rd-edition
Code License: MIT License
Note that the optional watermark extension is a small IPython notebook plugin that I developed to make the code reproducible. You can just skip the following line(s).
%load_ext watermark
%watermark -a "Sebastian Raschka & Vahid Mirjalili" -u -d -p numpy,scipy,matplotlib,tensorflow
Sebastian Raschka & Vahid Mirjalili last updated: 2019-12-06 numpy 1.17.4 scipy 1.3.1 matplotlib 3.1.0 tensorflow 2.0.0
from IPython.display import Image
%matplotlib inline
Image(filename='images/01.png', width=500)
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
## TF-v1.x style
g = tf.Graph()
with g.as_default():
a = tf.constant(1, name='a')
b = tf.constant(2, name='b')
c = tf.constant(3, name='c')
z = 2*(a - b) + c
with tf.compat.v1.Session(graph=g) as sess:
print('Result: z =', sess.run(z))
print('Result: z =', z.eval())
Result: z = 1 Result: z = 1
## TF v2 style
a = tf.constant(1, name='a')
b = tf.constant(2, name='b')
c = tf.constant(3, name='c')
z = 2*(a - b) + c
tf.print('Result: z =', z)
Result: z = 1
## TF-v1.x style
g = tf.Graph()
with g.as_default():
a = tf.compat.v1.placeholder(shape=None, dtype=tf.int32, name='tf_a')
b = tf.compat.v1.placeholder(shape=None, dtype=tf.int32, name='tf_b')
c = tf.compat.v1.placeholder(shape=None, dtype=tf.int32, name='tf_c')
z = 2*(a - b) + c
with tf.compat.v1.Session(graph=g) as sess:
feed_dict = {a:1, b:2, c:3}
print('Result: z =', sess.run(z, feed_dict=feed_dict))
Result: z = 1
## TF-v2 style
def compute_z(a, b, c):
r1 = tf.subtract(a, b)
r2 = tf.multiply(2, r1)
z = tf.add(r2, c)
return z
tf.print('Scalar Inputs:', compute_z(1, 2, 3))
tf.print('Rank 1 Inputs:', compute_z([1], [2], [3]))
tf.print('Rank 2 Inputs:', compute_z([[1]], [[2]], [[3]]))
Scalar Inputs: 1 Rank 1 Inputs: [1] Rank 2 Inputs: [[1]]
@tf.function
def compute_z(a, b, c):
r1 = tf.subtract(a, b)
r2 = tf.multiply(2, r1)
z = tf.add(r2, c)
return z
tf.print('Scalar Inputs:', compute_z(1, 2, 3))
tf.print('Rank 1 Inputs:', compute_z([1], [2], [3]))
tf.print('Rank 2 Inputs:', compute_z([[1]], [[2]], [[3]]))
Scalar Inputs: 1 Rank 1 Inputs: [1] Rank 2 Inputs: [[1]]
@tf.function(input_signature=(tf.TensorSpec(shape=[None], dtype=tf.int32),
tf.TensorSpec(shape=[None], dtype=tf.int32),
tf.TensorSpec(shape=[None], dtype=tf.int32),))
def compute_z(a, b, c):
r1 = tf.subtract(a, b)
r2 = tf.multiply(2, r1)
z = tf.add(r2, c)
return z
tf.print('Rank 1 Inputs:', compute_z([1], [2], [3]))
tf.print('Rank 1 Inputs:', compute_z([1, 2], [2, 4], [3, 6]))
Rank 1 Inputs: [1] Rank 1 Inputs: [1 2]
## we expect this to result in an error
tf.print('Rank 2 Inputs:', compute_z([[1], [2]], [[2], [4]], [[3], [6]]))
## >> Error:
#ValueError: Python inputs incompatible with input_signature:
#inputs (([[1], [2]], [[2], [4]], [[3], [6]])), input_signature
#((TensorSpec(shape=(None,), dtype=tf.int32, name=None),
# TensorSpec(shape=(None,), dtype=tf.int32, name=None),
# TensorSpec(shape=(None,), dtype=tf.int32, name=None)))
tf.TensorSpec(shape=[None], dtype=tf.int32)
TensorSpec(shape=(None,), dtype=tf.int32, name=None)
a = tf.Variable(initial_value=3.14, name='var_a')
b = tf.Variable(initial_value=[1, 2, 3], name='var_b')
c = tf.Variable(initial_value=[True, False], dtype=tf.bool)
d = tf.Variable(initial_value=['abc'], dtype=tf.string)
print(a)
print(b)
print(c)
print(d)
<tf.Variable 'var_a:0' shape=() dtype=float32, numpy=3.14> <tf.Variable 'var_b:0' shape=(3,) dtype=int32, numpy=array([1, 2, 3], dtype=int32)> <tf.Variable 'Variable:0' shape=(2,) dtype=bool, numpy=array([ True, False])> <tf.Variable 'Variable:0' shape=(1,) dtype=string, numpy=array([b'abc'], dtype=object)>
a.trainable
True
w = tf.Variable([1, 2, 3], trainable=False)
print(w.trainable)
False
print(w.assign([3, 1, 4], read_value=True))
w.assign_add([2, -1, 2], read_value=False)
print(w.value())
<tf.Variable 'UnreadVariable' shape=(3,) dtype=int32, numpy=array([3, 1, 4], dtype=int32)> tf.Tensor([5 0 6], shape=(3,), dtype=int32)
tf.random.set_seed(1)
init = tf.keras.initializers.GlorotNormal()
tf.print(init(shape=(3,)))
[-0.722795904 1.01456821 0.251808226]
v = tf.Variable(init(shape=(2, 3)))
tf.print(v)
[[0.28982234 -0.782292783 -0.0453658961] [0.960991383 -0.120003454 0.708528221]]
class MyModule(tf.Module):
def __init__(self):
init = tf.keras.initializers.GlorotNormal()
self.w1 = tf.Variable(init(shape=(2, 3)), trainable=True)
self.w2 = tf.Variable(init(shape=(1, 2)), trainable=False)
m = MyModule()
print('All module variables: ', [v.shape for v in m.variables])
print('Trainable variable: ', [v.shape for v in
m.trainable_variables])
All module variables: [TensorShape([2, 3]), TensorShape([1, 2])] Trainable variable: [TensorShape([2, 3])]
## this will produce an error
## ==> you cannot create a varibale inside a
## decorated function
@tf.function
def f(x):
w = tf.Variable([1, 2, 3])
f([1])
## ==> results in error
## ValueError: tf.function-decorated function tried to create variables on non-first call.
import tensorflow as tf
tf.random.set_seed(1)
w = tf.Variable(tf.random.uniform((3, 3)))
@tf.function
def compute_z(x):
return tf.matmul(w, x)
x = tf.constant([[1], [2], [3]], dtype=tf.float32)
tf.print(compute_z(x))
[[3.8610158] [2.94593048] [3.82629013]]
import tensorflow as tf
w = tf.Variable(1.0)
b = tf.Variable(0.5)
print(w.trainable, b.trainable)
x = tf.convert_to_tensor([1.4])
y = tf.convert_to_tensor([2.1])
with tf.GradientTape() as tape:
z = tf.add(tf.multiply(w, x), b)
loss = tf.reduce_sum(tf.square(y - z))
dloss_dw = tape.gradient(loss, w)
tf.print('dL/dw : ', dloss_dw)
True True dL/dw : -0.559999764
# verifying the computed gradient
#tf.print(-2*x * (-b - w*x + y))
tf.print(2*x * ((w*x + b) - y))
[-0.559999764]
Monitoring the non-trainable tensors via tape.watch()
with tf.GradientTape() as tape:
tape.watch(x)
z = tf.add(tf.multiply(w, x), b)
loss = tf.square(y - z)
dloss_dx = tape.gradient(loss, x)
tf.print('dL/dx:', dloss_dx)
dL/dx: [-0.399999857]
# verifying the computed gradient
tf.print(2*w * ((w*x + b) - y))
[-0.399999857]
via persistent=True
with tf.GradientTape(persistent=True) as tape:
z = tf.add(tf.multiply(w, x), b)
loss = tf.reduce_sum(tf.square(y - z))
dloss_dw = tape.gradient(loss, w)
dloss_db = tape.gradient(loss, b)
tf.print('dL/dw:', dloss_dw)
tf.print('dL/db:', dloss_db)
dL/dw: -0.559999764 dL/db: -0.399999857
optimizer.apply_gradients()
¶optimizer = tf.keras.optimizers.SGD()
optimizer.apply_gradients(zip([dloss_dw, dloss_db], [w, b]))
tf.print('Updated w:', w)
tf.print('Updated bias:', b)
Updated w: 1.0056 Updated bias: 0.504
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=16, activation='relu'))
model.add(tf.keras.layers.Dense(units=32, activation='relu'))
## late variable creation
model.build(input_shape=(None, 4))
model.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) multiple 80 _________________________________________________________________ dense_1 (Dense) multiple 544 ================================================================= Total params: 624 Trainable params: 624 Non-trainable params: 0 _________________________________________________________________
## printing variables of the model
for v in model.variables:
print('{:20s}'.format(v.name), v.trainable, v.shape)
dense/kernel:0 True (4, 16) dense/bias:0 True (16,) dense_1/kernel:0 True (16, 32) dense_1/bias:0 True (32,)
tf.keras.initializers
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/initializerstf.keras.regularizers
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/regularizerstf.keras.activations
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/activationsmodel = tf.keras.Sequential()
model.add(
tf.keras.layers.Dense(
units=16,
activation=tf.keras.activations.relu,
kernel_initializer=tf.keras.initializers.GlorotNormal(),
bias_initializer=tf.keras.initializers.Constant(2.0)
))
model.add(
tf.keras.layers.Dense(
units=32,
activation=tf.keras.activations.sigmoid,
kernel_regularizer=tf.keras.regularizers.l1
))
model.build(input_shape=(None, 4))
model.summary()
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_2 (Dense) multiple 80 _________________________________________________________________ dense_3 (Dense) multiple 544 ================================================================= Total params: 624 Trainable params: 624 Non-trainable params: 0 _________________________________________________________________
tf.keras.optimizers
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/optimizerstf.keras.losses
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/lossestf.keras.metrics
: https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/metricsmodel.compile(
optimizer=tf.keras.optimizers.SGD(learning_rate=0.001),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.Accuracy(),
tf.keras.metrics.Precision(),
tf.keras.metrics.Recall(),])
tf.random.set_seed(1)
np.random.seed(1)
x = np.random.uniform(low=-1, high=1, size=(200, 2))
y = np.ones(len(x))
y[x[:, 0] * x[:, 1]<0] = 0
x_train = x[:100, :]
y_train = y[:100]
x_valid = x[100:, :]
y_valid = y[100:]
fig = plt.figure(figsize=(6, 6))
plt.plot(x[y==0, 0],
x[y==0, 1], 'o', alpha=0.75, markersize=10)
plt.plot(x[y==1, 0],
x[y==1, 1], '<', alpha=0.75, markersize=10)
plt.xlabel(r'$x_1$', size=15)
plt.ylabel(r'$x_2$', size=15)
plt.show()
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=1,
input_shape=(2,),
activation='sigmoid'))
model.summary()
Model: "sequential_2" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_4 (Dense) (None, 1) 3 ================================================================= Total params: 3 Trainable params: 3 Non-trainable params: 0 _________________________________________________________________
model.compile(optimizer=tf.keras.optimizers.SGD(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy()])
hist = model.fit(x_train, y_train,
validation_data=(x_valid, y_valid),
epochs=200, batch_size=2, verbose=0)
from mlxtend.plotting import plot_decision_regions
history = hist.history
fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 3, 1)
plt.plot(history['loss'], lw=4)
plt.plot(history['val_loss'], lw=4)
plt.legend(['Train loss', 'Validation loss'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 2)
plt.plot(history['binary_accuracy'], lw=4)
plt.plot(history['val_binary_accuracy'], lw=4)
plt.legend(['Train Acc.', 'Validation Acc.'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 3)
plot_decision_regions(X=x_valid, y=y_valid.astype(np.integer),
clf=model)
ax.set_xlabel(r'$x_1$', size=15)
ax.xaxis.set_label_coords(1, -0.025)
ax.set_ylabel(r'$x_2$', size=15)
ax.yaxis.set_label_coords(-0.025, 1)
plt.show()
tf.random.set_seed(1)
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=4, input_shape=(2,), activation='relu'))
model.add(tf.keras.layers.Dense(units=4, activation='relu'))
model.add(tf.keras.layers.Dense(units=4, activation='relu'))
model.add(tf.keras.layers.Dense(units=1, activation='sigmoid'))
model.summary()
## compile:
model.compile(optimizer=tf.keras.optimizers.SGD(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy()])
## train:
hist = model.fit(x_train, y_train,
validation_data=(x_valid, y_valid),
epochs=200, batch_size=2, verbose=0)
history = hist.history
Model: "sequential_3" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_5 (Dense) (None, 4) 12 _________________________________________________________________ dense_6 (Dense) (None, 4) 20 _________________________________________________________________ dense_7 (Dense) (None, 4) 20 _________________________________________________________________ dense_8 (Dense) (None, 1) 5 ================================================================= Total params: 57 Trainable params: 57 Non-trainable params: 0 _________________________________________________________________
fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 3, 1)
plt.plot(history['loss'], lw=4)
plt.plot(history['val_loss'], lw=4)
plt.legend(['Train loss', 'Validation loss'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 2)
plt.plot(history['binary_accuracy'], lw=4)
plt.plot(history['val_binary_accuracy'], lw=4)
plt.legend(['Train Acc.', 'Validation Acc.'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 3)
plot_decision_regions(X=x_valid, y=y_valid.astype(np.integer),
clf=model)
ax.set_xlabel(r'$x_1$', size=15)
ax.xaxis.set_label_coords(1, -0.025)
ax.set_ylabel(r'$x_2$', size=15)
ax.yaxis.set_label_coords(-0.025, 1)
plt.show()
tf.random.set_seed(1)
## input layer:
inputs = tf.keras.Input(shape=(2,))
## hidden layers
h1 = tf.keras.layers.Dense(units=4, activation='relu')(inputs)
h2 = tf.keras.layers.Dense(units=4, activation='relu')(h1)
h3 = tf.keras.layers.Dense(units=4, activation='relu')(h2)
## output:
outputs = tf.keras.layers.Dense(units=1, activation='sigmoid')(h3)
## construct a model:
model = tf.keras.Model(inputs=inputs, outputs=outputs)
model.summary()
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 2)] 0 _________________________________________________________________ dense_9 (Dense) (None, 4) 12 _________________________________________________________________ dense_10 (Dense) (None, 4) 20 _________________________________________________________________ dense_11 (Dense) (None, 4) 20 _________________________________________________________________ dense_12 (Dense) (None, 1) 5 ================================================================= Total params: 57 Trainable params: 57 Non-trainable params: 0 _________________________________________________________________
## compile:
model.compile(optimizer=tf.keras.optimizers.SGD(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy()])
## train:
hist = model.fit(x_train, y_train,
validation_data=(x_valid, y_valid),
epochs=200, batch_size=2, verbose=0)
## Plotting
history = hist.history
fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 3, 1)
plt.plot(history['loss'], lw=4)
plt.plot(history['val_loss'], lw=4)
plt.legend(['Train loss', 'Validation loss'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 2)
plt.plot(history['binary_accuracy'], lw=4)
plt.plot(history['val_binary_accuracy'], lw=4)
plt.legend(['Train Acc.', 'Validation Acc.'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 3)
plot_decision_regions(X=x_valid, y=y_valid.astype(np.integer),
clf=model)
ax.set_xlabel(r'$x_1$', size=15)
ax.xaxis.set_label_coords(1, -0.025)
ax.set_ylabel(r'$x_2$', size=15)
ax.yaxis.set_label_coords(-0.025, 1)
plt.show()
class MyModel(tf.keras.Model):
def __init__(self):
super(MyModel, self).__init__()
self.hidden_1 = tf.keras.layers.Dense(units=4, activation='relu')
self.hidden_2 = tf.keras.layers.Dense(units=4, activation='relu')
self.hidden_3 = tf.keras.layers.Dense(units=4, activation='relu')
self.output_layer = tf.keras.layers.Dense(units=1, activation='sigmoid')
def call(self, inputs):
h = self.hidden_1(inputs)
h = self.hidden_2(h)
h = self.hidden_3(h)
return self.output_layer(h)
tf.random.set_seed(1)
## testing:
model = MyModel()
model.build(input_shape=(None, 2))
model.summary()
## compile:
model.compile(optimizer=tf.keras.optimizers.SGD(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy()])
## train:
hist = model.fit(x_train, y_train,
validation_data=(x_valid, y_valid),
epochs=200, batch_size=2, verbose=0)
## Plotting
history = hist.history
fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 3, 1)
plt.plot(history['loss'], lw=4)
plt.plot(history['val_loss'], lw=4)
plt.legend(['Train loss', 'Validation loss'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 2)
plt.plot(history['binary_accuracy'], lw=4)
plt.plot(history['val_binary_accuracy'], lw=4)
plt.legend(['Train Acc.', 'Validation Acc.'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 3)
plot_decision_regions(X=x_valid, y=y_valid.astype(np.integer),
clf=model)
ax.set_xlabel(r'$x_1$', size=15)
ax.xaxis.set_label_coords(1, -0.025)
ax.set_ylabel(r'$x_2$', size=15)
ax.yaxis.set_label_coords(-0.025, 1)
plt.show()
Model: "my_model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_13 (Dense) multiple 12 _________________________________________________________________ dense_14 (Dense) multiple 20 _________________________________________________________________ dense_15 (Dense) multiple 20 _________________________________________________________________ dense_16 (Dense) multiple 5 ================================================================= Total params: 57 Trainable params: 57 Non-trainable params: 0 _________________________________________________________________
class NoisyLinear(tf.keras.layers.Layer):
def __init__(self, output_dim, noise_stddev=0.1, **kwargs):
self.output_dim = output_dim
self.noise_stddev = noise_stddev
super(NoisyLinear, self).__init__(**kwargs)
def build(self, input_shape):
self.w = self.add_weight(name='weights',
shape=(input_shape[1], self.output_dim),
initializer='random_normal',
trainable=True)
self.b = self.add_weight(shape=(self.output_dim,),
initializer='zeros',
trainable=True)
def call(self, inputs, training=False):
if training:
batch = tf.shape(inputs)[0]
dim = tf.shape(inputs)[1]
noise = tf.random.normal(shape=(batch, dim),
mean=0.0,
stddev=self.noise_stddev)
noisy_inputs = tf.add(inputs, noise)
else:
noisy_inputs = inputs
z = tf.matmul(noisy_inputs, self.w) + self.b
return tf.keras.activations.relu(z)
def get_config(self):
config = super(NoisyLinear, self).get_config()
config.update({'output_dim': self.output_dim,
'noise_stddev': self.noise_stddev})
return config
## testing:
tf.random.set_seed(1)
noisy_layer = NoisyLinear(4)
noisy_layer.build(input_shape=(None, 4))
x = tf.zeros(shape=(1, 4))
tf.print(noisy_layer(x, training=True))
## re-building from config:
config = noisy_layer.get_config()
new_layer = NoisyLinear.from_config(config)
tf.print(new_layer(x, training=True))
[[0 0.00821428 0 0]] [[0 0.0108502861 0 0]]
tf.random.set_seed(1)
model = tf.keras.Sequential([
NoisyLinear(4, noise_stddev=0.1),
tf.keras.layers.Dense(units=4, activation='relu'),
tf.keras.layers.Dense(units=4, activation='relu'),
tf.keras.layers.Dense(units=1, activation='sigmoid')])
model.build(input_shape=(None, 2))
model.summary()
## compile:
model.compile(optimizer=tf.keras.optimizers.SGD(),
loss=tf.keras.losses.BinaryCrossentropy(),
metrics=[tf.keras.metrics.BinaryAccuracy()])
## train:
hist = model.fit(x_train, y_train,
validation_data=(x_valid, y_valid),
epochs=200, batch_size=2,
verbose=0)
## Plotting
history = hist.history
fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 3, 1)
plt.plot(history['loss'], lw=4)
plt.plot(history['val_loss'], lw=4)
plt.legend(['Train loss', 'Validation loss'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 2)
plt.plot(history['binary_accuracy'], lw=4)
plt.plot(history['val_binary_accuracy'], lw=4)
plt.legend(['Train Acc.', 'Validation Acc.'], fontsize=15)
ax.set_xlabel('Epochs', size=15)
ax = fig.add_subplot(1, 3, 3)
plot_decision_regions(X=x_valid, y=y_valid.astype(np.integer),
clf=model)
ax.set_xlabel(r'$x_1$', size=15)
ax.xaxis.set_label_coords(1, -0.025)
ax.set_ylabel(r'$x_2$', size=15)
ax.yaxis.set_label_coords(-0.025, 1)
plt.show()
Model: "sequential_4" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= noisy_linear_1 (NoisyLinear) multiple 12 _________________________________________________________________ dense_17 (Dense) multiple 20 _________________________________________________________________ dense_18 (Dense) multiple 20 _________________________________________________________________ dense_19 (Dense) multiple 5 ================================================================= Total params: 57 Trainable params: 57 Non-trainable params: 0 _________________________________________________________________
...
Readers may ignore the next cell.
! python ../.convert_notebook_to_script.py --input ch14_part1.ipynb --output ch14_part1.py
[NbConvertApp] Converting notebook ch14_part1.ipynb to script [NbConvertApp] Writing 19996 bytes to ch14_part1.py