มีคำถาม? เชื่อมต่อกับชุมชนที่ฟอรัม TensorFlow เยี่ยมชมฟอรัม

ถ่ายทอดการเรียนรู้และการปรับแต่ง

ดูใน TensorFlow.org เรียกใช้ใน Google Colab ดูแหล่งที่มาบน GitHub ดาวน์โหลดสมุดบันทึก

ติดตั้ง

import numpy as np
import tensorflow as tf
from tensorflow import keras

บทนำ

การถ่ายโอนการเรียนรู้ ประกอบด้วยการใช้คุณลักษณะที่เรียนรู้จากปัญหาหนึ่งและใช้ประโยชน์จากปัญหาใหม่ที่คล้ายคลึงกัน ตัวอย่างเช่นคุณสมบัติจากแบบจำลองที่เรียนรู้ที่จะระบุแรคคูนอาจมีประโยชน์ในการเริ่มต้นโมเดลที่มีไว้เพื่อระบุตัวทานูกิ

โดยปกติแล้วการถ่ายโอนการเรียนรู้จะทำสำหรับงานที่ชุดข้อมูลของคุณมีข้อมูลน้อยเกินไปที่จะฝึกโมเดลแบบเต็มตั้งแต่เริ่มต้น

การกำเนิดของการเรียนรู้แบบถ่ายโอนที่พบบ่อยที่สุดในบริบทของการเรียนรู้เชิงลึกคือขั้นตอนการทำงานต่อไปนี้:

  1. ใช้เลเยอร์จากแบบจำลองที่ได้รับการฝึกฝนมาก่อนหน้านี้
  2. ตรึงพวกเขาเพื่อหลีกเลี่ยงการทำลายข้อมูลใด ๆ ที่มีในระหว่างรอบการฝึกอบรมในอนาคต
  3. เพิ่มเลเยอร์ใหม่ที่สามารถฝึกได้ที่ด้านบนของเลเยอร์ที่ถูกแช่แข็ง พวกเขาจะเรียนรู้ที่จะเปลี่ยนคุณลักษณะเก่าให้เป็นการคาดคะเนในชุดข้อมูลใหม่
  4. ฝึกเลเยอร์ใหม่บนชุดข้อมูลของคุณ

ขั้นตอนสุดท้ายที่เป็นทางเลือกคือการ ปรับแต่ง อย่างละเอียดซึ่งประกอบด้วยการยกเลิกการแช่แข็งแบบจำลองทั้งหมดที่คุณได้รับด้านบน (หรือบางส่วน) และฝึกอบรมใหม่เกี่ยวกับข้อมูลใหม่ที่มีอัตราการเรียนรู้ต่ำ สิ่งนี้สามารถบรรลุการปรับปรุงที่มีความหมายได้โดยการปรับคุณสมบัติที่กำหนดไว้ล่วงหน้าให้เข้ากับข้อมูลใหม่

ครั้งแรกที่เราจะไปกว่า Keras trainable API ในรายละเอียดซึ่งรองรับการถ่ายโอนการเรียนรู้มากที่สุดและปรับจูนเวิร์กโฟลว์

จากนั้นเราจะสาธิตขั้นตอนการทำงานโดยทั่วไปโดยนำแบบจำลองที่ได้รับการฝึกฝนมาแล้วบนชุดข้อมูล ImageNet และฝึกอบรมใหม่ในชุดข้อมูลการจำแนกประเภท "แมวกับสุนัข" ของ Kaggle

สิ่งนี้ดัดแปลงมาจาก Deep Learning ด้วย Python และบล็อกโพสต์ปี 2016 "การสร้างโมเดลการจำแนกรูปภาพที่มีประสิทธิภาพโดยใช้ข้อมูลน้อยมาก"

ชั้นแช่แข็ง: การทำความเข้าใจ trainable แอตทริบิวต์

เลเยอร์และโมเดลมีคุณสมบัติน้ำหนักสามแบบ:

  • weights คือรายการของตัวแปรน้ำหนักทั้งหมดของเลเยอร์
  • trainable_weights คือรายการของสิ่งที่ตั้งใจจะอัปเดต (ผ่านการไล่ระดับสี) เพื่อลดการสูญเสียระหว่างการฝึก
  • non_trainable_weights คือรายชื่อของสิ่งที่ไม่ได้มีไว้สำหรับการฝึกอบรม โดยปกติแล้วจะมีการอัปเดตโดยโมเดลระหว่างการส่งต่อ

ตัวอย่าง: เลเยอร์ Dense มี 2 ​​น้ำหนักที่ฝึกได้ (เคอร์เนลและไบแอส)

layer = keras.layers.Dense(3)
layer.build((None, 4))  # Create the weights

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 2
trainable_weights: 2
non_trainable_weights: 0

โดยทั่วไปแล้วน้ำหนักทั้งหมดเป็นน้ำหนักที่ฝึกได้ เลเยอร์ในตัวเท่านั้นที่มีน้ำหนักที่ไม่สามารถฝึกได้คือเลเยอร์ BatchNormalization ใช้น้ำหนักที่ไม่สามารถฝึกได้เพื่อติดตามค่าเฉลี่ยและความแปรปรวนของอินพุตระหว่างการฝึก หากต้องการเรียนรู้วิธีใช้น้ำหนักที่ไม่สามารถฝึกได้ในเลเยอร์ที่คุณกำหนดเองโปรดดู คำแนะนำในการเขียนเลเยอร์ใหม่ตั้งแต่เริ่มต้น

ตัวอย่าง: ชั้น BatchNormalization มี 2 ​​น้ำหนักที่ฝึกได้และ 2 น้ำหนักที่ฝึกไม่ได้

layer = keras.layers.BatchNormalization()
layer.build((None, 4))  # Create the weights

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 4
trainable_weights: 2
non_trainable_weights: 2

ชั้นและรุ่นที่มีคุณลักษณะแบบบูล trainable ค่าของมันสามารถเปลี่ยนแปลงได้ การตั้งค่า layer.trainable เป็น False จะย้ายน้ำหนักของเลเยอร์ทั้งหมดจากแบบฝึกได้ไปเป็นแบบฝึกไม่ได้ สิ่งนี้เรียกว่า "การแช่แข็ง" เลเยอร์: สถานะของเลเยอร์ที่ถูกแช่แข็งจะไม่ได้รับการอัปเดตในระหว่างการฝึก (ไม่ว่าจะเมื่อฝึกด้วยความ fit() หรือเมื่อฝึกด้วยลูปที่กำหนดเองใด ๆ ที่อาศัย trainable_weights เพื่อใช้การอัปเดตไล่ระดับสี)

ตัวอย่าง: การตั้งค่า trainable การ False

layer = keras.layers.Dense(3)
layer.build((None, 4))  # Create the weights
layer.trainable = False  # Freeze the layer

print("weights:", len(layer.weights))
print("trainable_weights:", len(layer.trainable_weights))
print("non_trainable_weights:", len(layer.non_trainable_weights))
weights: 2
trainable_weights: 0
non_trainable_weights: 2

เมื่อน้ำหนักที่สามารถฝึกได้กลายเป็นน้ำหนักที่ไม่สามารถฝึกได้ค่าของน้ำหนักจะไม่อัปเดตในระหว่างการฝึกอีกต่อไป

# Make a model with 2 layers
layer1 = keras.layers.Dense(3, activation="relu")
layer2 = keras.layers.Dense(3, activation="sigmoid")
model = keras.Sequential([keras.Input(shape=(3,)), layer1, layer2])

# Freeze the first layer
layer1.trainable = False

# Keep a copy of the weights of layer1 for later reference
initial_layer1_weights_values = layer1.get_weights()

# Train the model
model.compile(optimizer="adam", loss="mse")
model.fit(np.random.random((2, 3)), np.random.random((2, 3)))

# Check that the weights of layer1 have not changed during training
final_layer1_weights_values = layer1.get_weights()
np.testing.assert_allclose(
    initial_layer1_weights_values[0], final_layer1_weights_values[0]
)
np.testing.assert_allclose(
    initial_layer1_weights_values[1], final_layer1_weights_values[1]
)
1/1 [==============================] - 1s 664ms/step - loss: 0.1025

อย่าสับสนระหว่างแอตทริบิวต์ layer.trainable กับการ training อาร์กิวเมนต์ใน layer.__call__() (ซึ่งควบคุมว่าเลเยอร์ควรรันการส่งต่อในโหมดอนุมานหรือโหมดการฝึกอบรม) สำหรับข้อมูลเพิ่มเติมโปรดดู คำถามที่พบบ่อยของ Keras

การตั้งค่าซ้ำของ trainable แอตทริบิวต์

หากคุณตั้งค่า trainable = False ในโมเดลหรือบนเลเยอร์ใด ๆ ที่มีเลเยอร์ย่อยเลเยอร์ย่อยทั้งหมดจะไม่สามารถฝึกได้เช่นกัน

ตัวอย่าง:

inner_model = keras.Sequential(
    [
        keras.Input(shape=(3,)),
        keras.layers.Dense(3, activation="relu"),
        keras.layers.Dense(3, activation="relu"),
    ]
)

model = keras.Sequential(
    [keras.Input(shape=(3,)), inner_model, keras.layers.Dense(3, activation="sigmoid"),]
)

model.trainable = False  # Freeze the outer model

assert inner_model.trainable == False  # All layers in `model` are now frozen
assert inner_model.layers[0].trainable == False  # `trainable` is propagated recursively

เวิร์กโฟลว์การถ่ายโอนการเรียนรู้ทั่วไป

สิ่งนี้นำเราไปสู่วิธีการใช้เวิร์กโฟลว์การเรียนรู้การถ่ายโอนทั่วไปใน Keras:

  1. สร้างโมเดลพื้นฐานและโหลดน้ำหนักที่ผ่านการฝึกอบรมมาแล้ว
  2. ตรึงเลเยอร์ทั้งหมดในโมเดลพื้นฐานโดยการตั้งค่า trainable = False
  3. สร้างโมเดลใหม่ที่ด้านบนของเอาต์พุตของเลเยอร์หนึ่ง (หรือหลายเลเยอร์) จากโมเดลพื้นฐาน
  4. ฝึกโมเดลใหม่ของคุณบนชุดข้อมูลใหม่ของคุณ

โปรดทราบว่าเวิร์กโฟลว์ทางเลือกอื่นที่มีน้ำหนักเบากว่าอาจเป็น:

  1. สร้างโมเดลพื้นฐานและโหลดน้ำหนักที่ผ่านการฝึกอบรมมาแล้ว
  2. เรียกใช้ชุดข้อมูลใหม่ของคุณและบันทึกผลลัพธ์ของเลเยอร์หนึ่ง (หรือหลายชั้น) จากแบบจำลองพื้นฐาน สิ่งนี้เรียกว่าการ แยกคุณลักษณะ
  3. ใช้เอาต์พุตนั้นเป็นข้อมูลอินพุตสำหรับโมเดลใหม่ที่เล็กกว่า

ข้อได้เปรียบที่สำคัญของเวิร์กโฟลว์ที่สองคือคุณเรียกใช้แบบจำลองพื้นฐานบนข้อมูลของคุณเพียงครั้งเดียวแทนที่จะเรียกใช้หนึ่งครั้งต่อหนึ่งครั้งของการฝึกอบรม ดังนั้นจึงเร็วกว่าและถูกกว่ามาก

อย่างไรก็ตามปัญหาเกี่ยวกับเวิร์กโฟลว์ที่สองคือไม่อนุญาตให้คุณแก้ไขข้อมูลอินพุตของโมเดลใหม่ของคุณแบบไดนามิกในระหว่างการฝึกอบรมซึ่งจำเป็นเมื่อทำการเพิ่มข้อมูลเป็นต้น โดยทั่วไปแล้วการเรียนรู้การถ่ายโอนจะใช้สำหรับงานเมื่อชุดข้อมูลใหม่ของคุณมีข้อมูลน้อยเกินไปที่จะฝึกโมเดลเต็มสเกลตั้งแต่เริ่มต้นและในสถานการณ์เช่นนี้การเพิ่มข้อมูลเป็นสิ่งสำคัญมาก ดังนั้นในสิ่งต่อไปนี้เราจะมุ่งเน้นไปที่เวิร์กโฟลว์แรก

นี่คือลักษณะของขั้นตอนการทำงานแรกใน Keras:

ขั้นแรกให้สร้างแบบจำลองพื้นฐานด้วยน้ำหนักที่ฝึกไว้ล่วงหน้า

base_model = keras.applications.Xception(
    weights='imagenet',  # Load weights pre-trained on ImageNet.
    input_shape=(150, 150, 3),
    include_top=False)  # Do not include the ImageNet classifier at the top.

จากนั้นตรึงโมเดลพื้นฐาน

base_model.trainable = False

สร้างโมเดลใหม่ที่ด้านบน

inputs = keras.Input(shape=(150, 150, 3))
# We make sure that the base_model is running in inference mode here,
# by passing `training=False`. This is important for fine-tuning, as you will
# learn in a few paragraphs.
x = base_model(inputs, training=False)
# Convert features of shape `base_model.output_shape[1:]` to vectors
x = keras.layers.GlobalAveragePooling2D()(x)
# A Dense classifier with a single unit (binary classification)
outputs = keras.layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

ฝึกโมเดลกับข้อมูลใหม่

model.compile(optimizer=keras.optimizers.Adam(),
              loss=keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=[keras.metrics.BinaryAccuracy()])
model.fit(new_dataset, epochs=20, callbacks=..., validation_data=...)

การปรับแต่งอย่างละเอียด

เมื่อโมเดลของคุณรวมเข้ากับข้อมูลใหม่แล้วคุณสามารถลองยกเลิกการตรึงทั้งหมดหรือบางส่วนของโมเดลพื้นฐานและฝึกโมเดลใหม่ทั้งหมดตั้งแต่ต้นจนจบด้วยอัตราการเรียนรู้ที่ต่ำมาก

นี่เป็นขั้นตอนสุดท้ายที่เป็นทางเลือกซึ่งอาจทำให้คุณได้รับการปรับปรุงเพิ่มเติม นอกจากนี้ยังอาจนำไปสู่การสวมใส่มากเกินไปอย่างรวดเร็ว - โปรดจำไว้ว่า

จำเป็นอย่างยิ่งที่จะต้องทำขั้นตอนนี้ หลังจาก ที่โมเดลที่มีเลเยอร์เยือกแข็งได้รับการฝึกฝนให้มาบรรจบกันแล้วเท่านั้น หากคุณผสมเลเยอร์ที่ฝึกได้เริ่มต้นแบบสุ่มกับเลเยอร์ที่ฝึกได้ซึ่งมีคุณสมบัติที่ฝึกฝนมาก่อนเลเยอร์ที่เริ่มต้นแบบสุ่มจะทำให้เกิดการอัปเดตการไล่ระดับสีที่มีขนาดใหญ่มากในระหว่างการฝึกซึ่งจะทำลายคุณสมบัติก่อนการฝึกอบรม

นอกจากนี้ยังจำเป็นอย่างยิ่งที่จะต้องใช้อัตราการเรียนรู้ที่ต่ำมากในขั้นตอนนี้เนื่องจากคุณกำลังฝึกโมเดลที่ใหญ่กว่าในการฝึกรอบแรกบนชุดข้อมูลที่โดยทั่วไปจะมีขนาดเล็กมาก ด้วยเหตุนี้คุณจึงมีความเสี่ยงที่จะสวมใส่มากเกินไปอย่างรวดเร็วหากคุณใช้การอัปเดตที่มีน้ำหนักมาก ที่นี่คุณต้องการอ่านเฉพาะน้ำหนักที่กำหนดไว้ล่วงหน้าด้วยวิธีที่เพิ่มขึ้น

นี่คือวิธีปรับใช้แบบจำลองพื้นฐานทั้งหมด:

# Unfreeze the base model
base_model.trainable = True

# It's important to recompile your model after you make any changes
# to the `trainable` attribute of any inner layer, so that your changes
# are take into account
model.compile(optimizer=keras.optimizers.Adam(1e-5),  # Very low learning rate
              loss=keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=[keras.metrics.BinaryAccuracy()])

# Train end-to-end. Be careful to stop before you overfit!
model.fit(new_dataset, epochs=10, callbacks=..., validation_data=...)

หมายเหตุสำคัญเกี่ยวกับ compile() และ trainable

การเรียก compile() บนโมเดลมีขึ้นเพื่อ "หยุด" พฤติกรรมของโมเดลนั้น ซึ่งหมายความว่า trainable ค่าแอตทริบิวต์ในเวลาแบบจะรวบรวมควรจะเก็บรักษาตลอดอายุการใช้งานของรูปแบบนั้นจนกระทั่ง compile ถูกเรียกอีกครั้ง ดังนั้นหากคุณเปลี่ยน trainable ค่าให้แน่ใจว่าจะโทร compile() อีกครั้งในรูปแบบของคุณสำหรับการเปลี่ยนแปลงของคุณจะถูกนำเข้าบัญชี

หมายเหตุสำคัญเกี่ยวกับเลเยอร์ BatchNormalization

โมเดลรูปภาพจำนวนมากมีเลเยอร์ BatchNormalization เลเยอร์นั้นเป็นกรณีพิเศษสำหรับทุกการนับเท่าที่จะจินตนาการได้ สิ่งที่ควรทราบมีดังนี้

  • BatchNormalization ประกอบด้วย 2 น้ำหนักที่ไม่สามารถฝึกได้ซึ่งได้รับการปรับปรุงระหว่างการฝึก ตัวแปรเหล่านี้คือตัวแปรติดตามค่าเฉลี่ยและความแปรปรวนของปัจจัยการผลิต
  • เมื่อคุณตั้งค่า bn_layer.trainable = False เลเยอร์ BatchNormalization จะทำงานในโหมดอนุมานและจะไม่อัปเดตค่าเฉลี่ยและสถิติความแปรปรวน นี่ไม่ใช่กรณีสำหรับเลเยอร์อื่น ๆ โดยทั่วไปเนื่องจากความ สามารถในการฝึกน้ำหนักและโหมดการอนุมาน / การฝึกอบรมเป็นแนวคิดสองแนวที่ตั้งฉาก กัน แต่ทั้งสองเชื่อมโยงกันในกรณีของเลเยอร์ BatchNormalization
  • เมื่อคุณยกเลิกการตรึงโมเดลที่มีเลเยอร์ BatchNormalization เพื่อทำการปรับแต่งคุณควรเก็บเลเยอร์ BatchNormalization ไว้ในโหมดอนุมานโดยผ่าน training=False เมื่อเรียกโมเดลพื้นฐาน มิฉะนั้นการอัปเดตที่ใช้กับน้ำหนักที่ไม่สามารถฝึกได้จะทำลายสิ่งที่โมเดลได้เรียนรู้ไปอย่างกะทันหัน

คุณจะเห็นรูปแบบนี้ใช้งานได้จริงในตัวอย่าง end-to-end ที่ส่วนท้ายของคู่มือนี้

ถ่ายทอดการเรียนรู้และการปรับแต่งด้วยลูปการฝึกอบรมที่กำหนดเอง

หากแทนที่จะเป็น fit() ว่าคุณกำลังใช้วงฝึกระดับต่ำของคุณเองขั้นตอนการทำงานจะยังคงเหมือนเดิม คุณควรระมัดระวังในการพิจารณาเฉพาะรูปแบบรายการ model.trainable_weights เมื่อใช้การอัปเดตการไล่ระดับสี:

# Create base model
base_model = keras.applications.Xception(
    weights='imagenet',
    input_shape=(150, 150, 3),
    include_top=False)
# Freeze base model
base_model.trainable = False

# Create new model on top.
inputs = keras.Input(shape=(150, 150, 3))
x = base_model(inputs, training=False)
x = keras.layers.GlobalAveragePooling2D()(x)
outputs = keras.layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)
optimizer = keras.optimizers.Adam()

# Iterate over the batches of a dataset.
for inputs, targets in new_dataset:
    # Open a GradientTape.
    with tf.GradientTape() as tape:
        # Forward pass.
        predictions = model(inputs)
        # Compute the loss value for this batch.
        loss_value = loss_fn(targets, predictions)

    # Get gradients of loss wrt the *trainable* weights.
    gradients = tape.gradient(loss_value, model.trainable_weights)
    # Update the weights of the model.
    optimizer.apply_gradients(zip(gradients, model.trainable_weights))

ในทำนองเดียวกันสำหรับการปรับแต่ง

ตัวอย่าง end-to-end: การปรับโมเดลการจำแนกรูปภาพบนชุดข้อมูลแมวกับสุนัข

เพื่อเสริมสร้างแนวคิดเหล่านี้เราจะแนะนำคุณเกี่ยวกับตัวอย่างการเรียนรู้การถ่ายโอนจากต้นทางถึงปลายทางที่เป็นรูปธรรมและการปรับแต่งอย่างละเอียด เราจะโหลดโมเดล Xception ซึ่งได้รับการฝึกฝนมาแล้วบน ImageNet และใช้กับชุดข้อมูลการจำแนกประเภท "แมวกับสุนัข" ของ Kaggle

รับข้อมูล

ขั้นแรกให้ดึงข้อมูลชุดแมวกับสุนัขโดยใช้ TFDS หากคุณมีชุดข้อมูลของคุณเองคุณอาจต้องการใช้ยูทิลิตี้ tf.keras.preprocessing.image_dataset_from_directory เพื่อสร้างวัตถุชุดข้อมูลที่มีป้ายกำกับที่คล้ายกันจากชุดของภาพบนดิสก์ที่เก็บไว้ในโฟลเดอร์เฉพาะคลาส

การถ่ายโอนการเรียนรู้มีประโยชน์มากที่สุดเมื่อทำงานกับชุดข้อมูลขนาดเล็กมาก เพื่อให้ชุดข้อมูลของเรามีขนาดเล็กเราจะใช้ 40% ของข้อมูลการฝึกอบรมเดิม (25,000 ภาพ) สำหรับการฝึกอบรม 10% สำหรับการตรวจสอบความถูกต้องและ 10% สำหรับการทดสอบ

import tensorflow_datasets as tfds

tfds.disable_progress_bar()

train_ds, validation_ds, test_ds = tfds.load(
    "cats_vs_dogs",
    # Reserve 10% for validation and 10% for test
    split=["train[:40%]", "train[40%:50%]", "train[50%:60%]"],
    as_supervised=True,  # Include labels
)

print("Number of training samples: %d" % tf.data.experimental.cardinality(train_ds))
print(
    "Number of validation samples: %d" % tf.data.experimental.cardinality(validation_ds)
)
print("Number of test samples: %d" % tf.data.experimental.cardinality(test_ds))
Number of training samples: 9305
Number of validation samples: 2326
Number of test samples: 2326

นี่คือภาพ 9 ภาพแรกในชุดข้อมูลการฝึกอบรม - อย่างที่คุณเห็นมีขนาดต่างกันทั้งหมด

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 10))
for i, (image, label) in enumerate(train_ds.take(9)):
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(image)
    plt.title(int(label))
    plt.axis("off")

png

นอกจากนี้เรายังสามารถดูได้ว่าป้ายกำกับ 1 คือ "สุนัข" และป้ายกำกับ 0 คือ "แมว"

การกำหนดมาตรฐานข้อมูล

ภาพดิบของเรามีหลากหลายขนาด นอกจากนี้แต่ละพิกเซลยังประกอบด้วยค่าจำนวนเต็ม 3 ค่าระหว่าง 0 ถึง 255 (ค่าระดับ RGB) นี่ไม่เหมาะอย่างยิ่งสำหรับการให้อาหารเครือข่ายประสาท เราต้องทำ 2 สิ่ง:

  • กำหนดมาตรฐานเป็นขนาดภาพคงที่ เราเลือก 150x150
  • ปรับค่าพิกเซลให้เป็นมาตรฐานระหว่าง -1 ถึง 1 เราจะทำสิ่งนี้โดยใช้เลเยอร์ Normalization เป็นส่วนหนึ่งของโมเดล

โดยทั่วไปเป็นแนวทางปฏิบัติที่ดีในการพัฒนาแบบจำลองที่ใช้ข้อมูลดิบเป็นอินพุตซึ่งต่างจากโมเดลที่ใช้ข้อมูลที่ประมวลผลล่วงหน้าแล้ว ด้วยเหตุนี้หากโมเดลของคุณต้องการข้อมูลที่ผ่านการประมวลผลล่วงหน้าทุกครั้งที่คุณส่งออกโมเดลของคุณไปใช้ที่อื่น (ในเว็บเบราว์เซอร์ในแอปบนอุปกรณ์เคลื่อนที่) คุณจะต้องติดตั้งไปป์ไลน์การประมวลผลล่วงหน้าที่เหมือนกันทุกประการ สิ่งนี้จะยุ่งยากมากอย่างรวดเร็ว ดังนั้นเราควรทำการประมวลผลล่วงหน้าให้น้อยที่สุดเท่าที่จะเป็นไปได้ก่อนที่จะกดปุ่มโมเดล

ที่นี่เราจะทำการปรับขนาดภาพในไปป์ไลน์ข้อมูล (เนื่องจากเครือข่ายประสาทเทียมระดับลึกสามารถประมวลผลชุดข้อมูลที่ต่อเนื่องกันเท่านั้น) และเราจะทำการปรับขนาดค่าอินพุตเป็นส่วนหนึ่งของแบบจำลองเมื่อเราสร้าง

มาปรับขนาดภาพเป็น 150x150:

size = (150, 150)

train_ds = train_ds.map(lambda x, y: (tf.image.resize(x, size), y))
validation_ds = validation_ds.map(lambda x, y: (tf.image.resize(x, size), y))
test_ds = test_ds.map(lambda x, y: (tf.image.resize(x, size), y))

นอกจากนี้เรามาจัดกลุ่มข้อมูลและใช้การแคชและการดึงข้อมูลล่วงหน้าเพื่อเพิ่มประสิทธิภาพความเร็วในการโหลด

batch_size = 32

train_ds = train_ds.cache().batch(batch_size).prefetch(buffer_size=10)
validation_ds = validation_ds.cache().batch(batch_size).prefetch(buffer_size=10)
test_ds = test_ds.cache().batch(batch_size).prefetch(buffer_size=10)

ใช้การเพิ่มข้อมูลแบบสุ่ม

เมื่อคุณไม่มีชุดข้อมูลรูปภาพขนาดใหญ่เป็นแนวทางปฏิบัติที่ดีในการนำเสนอความหลากหลายของตัวอย่างโดยใช้การแปลงแบบสุ่ม แต่เหมือนจริงกับภาพการฝึกเช่นการพลิกแนวนอนแบบสุ่มหรือการหมุนแบบสุ่มขนาดเล็ก ซึ่งจะช่วยให้โมเดลได้รับข้อมูลการฝึกในแง่มุมต่างๆในขณะที่ชะลอการฟิตติ้งมากเกินไป

from tensorflow import keras
from tensorflow.keras import layers

data_augmentation = keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),
        layers.experimental.preprocessing.RandomRotation(0.1),
    ]
)

มาดูกันว่าภาพแรกของชุดแรกมีลักษณะอย่างไรหลังจากการแปลงแบบสุ่มต่างๆ:

import numpy as np

for images, labels in train_ds.take(1):
    plt.figure(figsize=(10, 10))
    first_image = images[0]
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1)
        augmented_image = data_augmentation(
            tf.expand_dims(first_image, 0), training=True
        )
        plt.imshow(augmented_image[0].numpy().astype("int32"))
        plt.title(int(labels[0]))
        plt.axis("off")

png

สร้างแบบจำลอง

ตอนนี้เรามาสร้างแบบจำลองที่เป็นไปตามพิมพ์เขียวที่เราได้อธิบายไว้ก่อนหน้านี้

โปรดทราบว่า:

  • เราเพิ่มเลเยอร์ Normalization เพื่อปรับขนาดค่าอินพุต (เริ่มแรกในช่วง [0, 255] ) ไปยังช่วง [-1, 1]
  • เราเพิ่มเลเยอร์ Dropout ก่อนเลเยอร์การจัดหมวดหมู่เพื่อทำให้เป็นมาตรฐาน
  • เราตรวจสอบให้แน่ใจว่าผ่าน training=False เมื่อเรียกโมเดลพื้นฐานเพื่อให้ทำงานในโหมดการอนุมานดังนั้นสถิติแบทช์นอร์มจะไม่ได้รับการอัปเดตแม้ว่าเราจะยกเลิกการตรึงโมเดลพื้นฐานสำหรับการปรับแต่งแล้วก็ตาม
base_model = keras.applications.Xception(
    weights="imagenet",  # Load weights pre-trained on ImageNet.
    input_shape=(150, 150, 3),
    include_top=False,
)  # Do not include the ImageNet classifier at the top.

# Freeze the base_model
base_model.trainable = False

# Create new model on top
inputs = keras.Input(shape=(150, 150, 3))
x = data_augmentation(inputs)  # Apply random data augmentation

# Pre-trained Xception weights requires that input be normalized
# from (0, 255) to a range (-1., +1.), the normalization layer
# does the following, outputs = (inputs - mean) / sqrt(var)
norm_layer = keras.layers.experimental.preprocessing.Normalization()
mean = np.array([127.5] * 3)
var = mean ** 2
# Scale inputs to [-1, +1]
x = norm_layer(x)
norm_layer.set_weights([mean, var])

# The base model contains batchnorm layers. We want to keep them in inference mode
# when we unfreeze the base model for fine-tuning, so we make sure that the
# base_model is running in inference mode here.
x = base_model(x, training=False)
x = keras.layers.GlobalAveragePooling2D()(x)
x = keras.layers.Dropout(0.2)(x)  # Regularize with dropout
outputs = keras.layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

model.summary()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/xception/xception_weights_tf_dim_ordering_tf_kernels_notop.h5
83689472/83683744 [==============================] - 1s 0us/step
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_5 (InputLayer)         [(None, 150, 150, 3)]     0         
_________________________________________________________________
sequential_3 (Sequential)    (None, 150, 150, 3)       0         
_________________________________________________________________
normalization (Normalization (None, 150, 150, 3)       7         
_________________________________________________________________
xception (Functional)        (None, 5, 5, 2048)        20861480  
_________________________________________________________________
global_average_pooling2d (Gl (None, 2048)              0         
_________________________________________________________________
dropout (Dropout)            (None, 2048)              0         
_________________________________________________________________
dense_7 (Dense)              (None, 1)                 2049      
=================================================================
Total params: 20,863,536
Trainable params: 2,049
Non-trainable params: 20,861,487
_________________________________________________________________

ฝึกเลเยอร์บนสุด

model.compile(
    optimizer=keras.optimizers.Adam(),
    loss=keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=[keras.metrics.BinaryAccuracy()],
)

epochs = 20
model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
Epoch 1/20
291/291 [==============================] - 20s 49ms/step - loss: 0.2226 - binary_accuracy: 0.8972 - val_loss: 0.0805 - val_binary_accuracy: 0.9703
Epoch 2/20
291/291 [==============================] - 8s 29ms/step - loss: 0.1246 - binary_accuracy: 0.9464 - val_loss: 0.0757 - val_binary_accuracy: 0.9712
Epoch 3/20
291/291 [==============================] - 8s 29ms/step - loss: 0.1153 - binary_accuracy: 0.9480 - val_loss: 0.0724 - val_binary_accuracy: 0.9733
Epoch 4/20
291/291 [==============================] - 8s 29ms/step - loss: 0.1055 - binary_accuracy: 0.9575 - val_loss: 0.0753 - val_binary_accuracy: 0.9721
Epoch 5/20
291/291 [==============================] - 8s 28ms/step - loss: 0.1026 - binary_accuracy: 0.9589 - val_loss: 0.0750 - val_binary_accuracy: 0.9703
Epoch 6/20
291/291 [==============================] - 8s 28ms/step - loss: 0.1022 - binary_accuracy: 0.9587 - val_loss: 0.0723 - val_binary_accuracy: 0.9716
Epoch 7/20
291/291 [==============================] - 8s 28ms/step - loss: 0.1009 - binary_accuracy: 0.9570 - val_loss: 0.0731 - val_binary_accuracy: 0.9708
Epoch 8/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0947 - binary_accuracy: 0.9576 - val_loss: 0.0726 - val_binary_accuracy: 0.9716
Epoch 9/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0872 - binary_accuracy: 0.9624 - val_loss: 0.0720 - val_binary_accuracy: 0.9712
Epoch 10/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0892 - binary_accuracy: 0.9622 - val_loss: 0.0711 - val_binary_accuracy: 0.9716
Epoch 11/20
291/291 [==============================] - 8s 29ms/step - loss: 0.0987 - binary_accuracy: 0.9608 - val_loss: 0.0752 - val_binary_accuracy: 0.9712
Epoch 12/20
291/291 [==============================] - 8s 29ms/step - loss: 0.0962 - binary_accuracy: 0.9595 - val_loss: 0.0715 - val_binary_accuracy: 0.9738
Epoch 13/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0972 - binary_accuracy: 0.9606 - val_loss: 0.0700 - val_binary_accuracy: 0.9725
Epoch 14/20
291/291 [==============================] - 8s 28ms/step - loss: 0.1019 - binary_accuracy: 0.9568 - val_loss: 0.0779 - val_binary_accuracy: 0.9690
Epoch 15/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0929 - binary_accuracy: 0.9614 - val_loss: 0.0700 - val_binary_accuracy: 0.9729
Epoch 16/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0937 - binary_accuracy: 0.9610 - val_loss: 0.0698 - val_binary_accuracy: 0.9742
Epoch 17/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0945 - binary_accuracy: 0.9613 - val_loss: 0.0671 - val_binary_accuracy: 0.9759
Epoch 18/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0868 - binary_accuracy: 0.9612 - val_loss: 0.0692 - val_binary_accuracy: 0.9738
Epoch 19/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0871 - binary_accuracy: 0.9647 - val_loss: 0.0691 - val_binary_accuracy: 0.9746
Epoch 20/20
291/291 [==============================] - 8s 28ms/step - loss: 0.0922 - binary_accuracy: 0.9603 - val_loss: 0.0721 - val_binary_accuracy: 0.9738
<tensorflow.python.keras.callbacks.History at 0x7fb73f231860>

ทำการปรับจูนโมเดลทั้งหมดอย่างละเอียด

สุดท้ายเรามาเลิกตรึงโมเดลพื้นฐานและฝึกโมเดลทั้งหมดตั้งแต่ต้นจนจบด้วยอัตราการเรียนรู้ที่ต่ำ

ที่สำคัญแม้ว่าโมเดลพื้นฐานจะสามารถฝึกได้ แต่ก็ยังคงทำงานในโหมดอนุมานเนื่องจากเราผ่าน training=False เมื่อเรียกมันเมื่อเราสร้างโมเดล ซึ่งหมายความว่าเลเยอร์การทำให้เป็นมาตรฐานของแบตช์ที่อยู่ภายในจะไม่อัปเดตสถิติแบตช์ หากเป็นเช่นนั้นพวกเขาจะสร้างความเสียหายให้กับตัวแทนที่ได้เรียนรู้จากแบบจำลองจนถึงตอนนี้

# Unfreeze the base_model. Note that it keeps running in inference mode
# since we passed `training=False` when calling it. This means that
# the batchnorm layers will not update their batch statistics.
# This prevents the batchnorm layers from undoing all the training
# we've done so far.
base_model.trainable = True
model.summary()

model.compile(
    optimizer=keras.optimizers.Adam(1e-5),  # Low learning rate
    loss=keras.losses.BinaryCrossentropy(from_logits=True),
    metrics=[keras.metrics.BinaryAccuracy()],
)

epochs = 10
model.fit(train_ds, epochs=epochs, validation_data=validation_ds)
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_5 (InputLayer)         [(None, 150, 150, 3)]     0         
_________________________________________________________________
sequential_3 (Sequential)    (None, 150, 150, 3)       0         
_________________________________________________________________
normalization (Normalization (None, 150, 150, 3)       7         
_________________________________________________________________
xception (Functional)        (None, 5, 5, 2048)        20861480  
_________________________________________________________________
global_average_pooling2d (Gl (None, 2048)              0         
_________________________________________________________________
dropout (Dropout)            (None, 2048)              0         
_________________________________________________________________
dense_7 (Dense)              (None, 1)                 2049      
=================================================================
Total params: 20,863,536
Trainable params: 20,809,001
Non-trainable params: 54,535
_________________________________________________________________
Epoch 1/10
291/291 [==============================] - 43s 133ms/step - loss: 0.0814 - binary_accuracy: 0.9677 - val_loss: 0.0527 - val_binary_accuracy: 0.9776
Epoch 2/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0544 - binary_accuracy: 0.9796 - val_loss: 0.0537 - val_binary_accuracy: 0.9776
Epoch 3/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0481 - binary_accuracy: 0.9822 - val_loss: 0.0471 - val_binary_accuracy: 0.9789
Epoch 4/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0324 - binary_accuracy: 0.9871 - val_loss: 0.0551 - val_binary_accuracy: 0.9807
Epoch 5/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0298 - binary_accuracy: 0.9899 - val_loss: 0.0447 - val_binary_accuracy: 0.9807
Epoch 6/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0262 - binary_accuracy: 0.9901 - val_loss: 0.0469 - val_binary_accuracy: 0.9824
Epoch 7/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0242 - binary_accuracy: 0.9918 - val_loss: 0.0539 - val_binary_accuracy: 0.9798
Epoch 8/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0153 - binary_accuracy: 0.9935 - val_loss: 0.0644 - val_binary_accuracy: 0.9794
Epoch 9/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0175 - binary_accuracy: 0.9934 - val_loss: 0.0496 - val_binary_accuracy: 0.9819
Epoch 10/10
291/291 [==============================] - 38s 129ms/step - loss: 0.0171 - binary_accuracy: 0.9936 - val_loss: 0.0496 - val_binary_accuracy: 0.9828
<tensorflow.python.keras.callbacks.History at 0x7fb74f74f940>

หลังจากผ่านไป 10 ยุคการปรับแต่งอย่างละเอียดทำให้เราได้รับการปรับปรุงที่ดีที่นี่