עזרה להגן על שונית המחסום הגדולה עם TensorFlow על Kaggle הצטרפו אתגר

אלגוריתמים מותאמים אישית, חלק 2: יישום ממוצע של פדרציה

הצג באתר TensorFlow.org הפעל בגוגל קולאב צפה במקור ב-GitHub הורד מחברת

הדרכה זו היא החלק השני של סדרה בת שני חלקים המדגים כיצד ליישם סוגים מותאמים אישית של אלגוריתמים Federated ב TFF באמצעות Core Federated (FC) , אשר משמש כבסיס עבור Federated למידה (FL) שכבת ( tff.learning ) .

אנו ממליצים לך לקרוא את הראשון בחלק הראשון של הסדרה הזו , אשר מציגה כמה מושגי מפתח הפשטות תכנות משמשות כאן.

החלק השני של הסדרה משתמש במנגנונים שהוצגו בחלק הראשון כדי ליישם גרסה פשוטה של ​​אלגוריתמי אימון והערכה מאוחדים.

אנו ממליצים לך לבדוק את סיווג תמונה ואת דור טקסט הדרכות עבור רמה גבוהה יותר והכנסה עדינה יותר APIs למידה Federated של TFF, כפי שהם יעזרו לך לשים את המושגים שאנו מתארים כאן בהקשר.

לפני שאנחנו מתחילים

לפני שנתחיל, נסה להפעיל את הדוגמה הבאה של "Hello World" כדי לוודא שהסביבה שלך מוגדרת כהלכה. אם זה לא עובד, אנא פנה אלי התקנת המדריך לקבלת הוראות.

!pip install --quiet --upgrade tensorflow-federated-nightly
!pip install --quiet --upgrade nest-asyncio

import nest_asyncio
nest_asyncio.apply()
import collections

import numpy as np
import tensorflow as tf
import tensorflow_federated as tff

# Must use the Python context because it
# supports tff.sequence_* intrinsics.
executor_factory = tff.framework.local_executor_factory(
    support_sequence_ops=True)
execution_context = tff.framework.ExecutionContext(
    executor_fn=executor_factory)
tff.framework.set_default_context(execution_context)
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

יישום ממוצע פדרלי

כמו Federated למידה עבור סיווג תמונה , אנחנו הולכים להשתמש בדוגמא MNIST, אבל מאז זה מיועד לשמש הדרכה ברמה נמוכה, אנחנו הולכים לעקוף את Keras API ו- tff.simulation , לכתוב קוד דגם גלם, וכן מבנה מערך נתונים מאוחד מאפס.

הכנת מערכי נתונים מאוחדים

לצורך הדגמה, אנו הולכים לדמות תרחיש בו יש לנו נתונים מ-10 משתמשים, וכל אחד מהמשתמשים תורם ידע כיצד לזהות ספרה אחרת. זהו בערך כמו אי iid כפי שהוא מקבל.

ראשית, בואו נטען את נתוני MNIST הסטנדרטיים:

mnist_train, mnist_test = tf.keras.datasets.mnist.load_data()
[(x.dtype, x.shape) for x in mnist_train]
[(dtype('uint8'), (60000, 28, 28)), (dtype('uint8'), (60000,))]

הנתונים מגיעים כמערכי Numpy, אחד עם תמונות ואחר עם תוויות ספרות, שניהם כשהמימד הראשון עובר על הדוגמאות הבודדות. בוא נכתוב פונקציית מסייעת שמעצבת אותה בצורה שתואמת לאופן שבו אנו מזינים רצפים מאוחדים לתוך חישובי TFF, כלומר, כרשימה של רשימות - הרשימה החיצונית נעה על פני המשתמשים (ספרות), הפנימיות על פני אצווה של נתונים ב הרצף של כל לקוח. כמקובל, נוכל לארגן כל אחד אצווה כמו זוג tensors בשם x ו- y , כל אחד עם הממד אצווה המובילים. בעוד על זה, נצטרך גם לשטח כל תמונה לתוך וקטור 784, אלמנט rescale הפיקסלים אותו לתוך 0..1 טווח, כך שאין לנו להעמיס את ההיגיון דגם עם המרות נתונים.

NUM_EXAMPLES_PER_USER = 1000
BATCH_SIZE = 100


def get_data_for_digit(source, digit):
  output_sequence = []
  all_samples = [i for i, d in enumerate(source[1]) if d == digit]
  for i in range(0, min(len(all_samples), NUM_EXAMPLES_PER_USER), BATCH_SIZE):
    batch_samples = all_samples[i:i + BATCH_SIZE]
    output_sequence.append({
        'x':
            np.array([source[0][i].flatten() / 255.0 for i in batch_samples],
                     dtype=np.float32),
        'y':
            np.array([source[1][i] for i in batch_samples], dtype=np.int32)
    })
  return output_sequence


federated_train_data = [get_data_for_digit(mnist_train, d) for d in range(10)]

federated_test_data = [get_data_for_digit(mnist_test, d) for d in range(10)]

כבדיקה השפיות מהירה, מראה ונודה על Y מותח את המנה האחרונה של נתונים שנתרמו על ידי הלקוח החמישי (האחד המתאים הספרה 5 ).

federated_train_data[5][-1]['y']
array([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
       5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], dtype=int32)

רק ליתר ביטחון, בואו נסתכל גם על התמונה המתאימה לרכיב האחרון של אותה אצווה.

from matplotlib import pyplot as plt

plt.imshow(federated_train_data[5][-1]['x'][-1].reshape(28, 28), cmap='gray')
plt.grid(False)
plt.show()

png

על שילוב של TensorFlow ו-TFF

במדריך זה, עבור קומפקטיות אנחנו מייד לקשט פונקציות להציג היגיון TensorFlow עם tff.tf_computation . עם זאת, עבור היגיון מורכב יותר, זה לא הדפוס שאנו ממליצים עליו. איתור באגים של TensorFlow כבר יכול להיות אתגר, ואיתור באגים של TensorFlow לאחר שבוצע בסידרה מלאה ולאחר מכן יבוא מחדש מאבד בהכרח כמה מטא נתונים ומגביל את האינטראקטיביות, מה שהופך את ניפוי הבאגים לאתגר עוד יותר.

לכן, אנו ממליצים בחום כתיבת היגיון TF מורכב כמו פונקציות Python עצמאיות (כלומר, ללא tff.tf_computation קישוט). ניתן לפתח בדרך זו את היגיון TensorFlow ונבדק באמצעות שיטות עבודה מומלצות TF וכלים (כמו במצב להוט), לפני בהמשכים את החישוב עבור TFF (למשל, על ידי פניית tff.tf_computation עם פונקצית Python כוויכוח).

הגדרת פונקציית אובדן

עכשיו כשיש לנו את הנתונים, בואו נגדיר פונקציית אובדן שבה נוכל להשתמש לאימון. ראשית, הבה נגדיר את סוג הקלט כ-TFF בשם tuple. מאז גודל של קבוצות הנתונים עשויים להשתנות, אנו קובעים את הממד אצווה כדי None כדי לציין את הגודל של המימד הזה אינו ידוע.

BATCH_SPEC = collections.OrderedDict(
    x=tf.TensorSpec(shape=[None, 784], dtype=tf.float32),
    y=tf.TensorSpec(shape=[None], dtype=tf.int32))
BATCH_TYPE = tff.to_type(BATCH_SPEC)

str(BATCH_TYPE)
'<x=float32[?,784],y=int32[?]>'

אתה אולי תוהה למה אנחנו לא יכולים פשוט להגדיר סוג פייתון רגיל. תזכיר את הדיון ב חלק 1 , שבו הסברנו כי בעוד אנחנו יכולים להביע את ההיגיון של חישובי TFF באמצעות Python, תחת חישובי TFF הברדס איננו Python. סמל BATCH_TYPE כמוגדר לעיל מייצג מפרט סוג TFF מופשט. חשוב להבחין בסוג TFF המופשט הזה מסוגים ייצוג קונקרטי Python, למשל, מכולות כגון dict או collections.namedtuple שעשויים לשמש לייצוג סוג TFF בגוף של פונקציה Python. בניגוד Python, יש TFF בנאי סוג מופשט יחיד tff.StructType עבור tuple דמוי מכולות, עם אלמנטים שיכולים להיקרא בנפרד או שמאלה ללא שם. סוג זה משמש גם למודל של פרמטרים פורמליים של חישובים, שכן חישובי TFF יכולים להכריז רשמית רק על פרמטר אחד ותוצאה אחת - תראה דוגמאות לכך בקרוב.

בואו עכשיו להגדיר את סוג TFF של הפרמטרים במודל, שוב בתור tuple בשם TFF של משקולות הטיה.

MODEL_SPEC = collections.OrderedDict(
    weights=tf.TensorSpec(shape=[784, 10], dtype=tf.float32),
    bias=tf.TensorSpec(shape=[10], dtype=tf.float32))
MODEL_TYPE = tff.to_type(MODEL_SPEC)
print(MODEL_TYPE)
<weights=float32[784,10],bias=float32[10]>

עם הגדרות אלו, כעת אנו יכולים להגדיר את ההפסד עבור מודל נתון, על פני אצווה בודדת. הערת השימוש @tf.function מעצב בתוך @tff.tf_computation המעצב. זה מאפשר לנו לכתוב TF באמצעות Python כמו סמנטיקה אף היו בתוך tf.Graph בהקשר שיצר tff.tf_computation המעצב.

# NOTE: `forward_pass` is defined separately from `batch_loss` so that it can 
# be later called from within another tf.function. Necessary because a
# @tf.function  decorated method cannot invoke a @tff.tf_computation.

@tf.function
def forward_pass(model, batch):
  predicted_y = tf.nn.softmax(
      tf.matmul(batch['x'], model['weights']) + model['bias'])
  return -tf.reduce_mean(
      tf.reduce_sum(
          tf.one_hot(batch['y'], 10) * tf.math.log(predicted_y), axis=[1]))

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE)
def batch_loss(model, batch):
  return forward_pass(model, batch)

כצפוי, חישוב batch_loss מחזיר float32 הפסד בהינתן המודל יצווה נתונים יחידים. שים לב איך MODEL_TYPE ו BATCH_TYPE כבר מקובץ יחדיו לכדי-tuple 2 פרמטרים פורמלית; אתה יכול לזהות את סוג batch_loss כמו (<MODEL_TYPE,BATCH_TYPE> -> float32) .

str(batch_loss.type_signature)
'(<model=<weights=float32[784,10],bias=float32[10]>,batch=<x=float32[?,784],y=int32[?]>> -> float32)'

כבדיקת שפיות, בואו נבנה מודל ראשוני מלא באפסים ונחשב את האובדן על קבוצת הנתונים שדמיינו למעלה.

initial_model = collections.OrderedDict(
    weights=np.zeros([784, 10], dtype=np.float32),
    bias=np.zeros([10], dtype=np.float32))

sample_batch = federated_train_data[5][-1]

batch_loss(initial_model, sample_batch)
2.3025851

הערה שאנחנו להאכיל את החישוב TFF עם המודל הראשוני שהוגדר בתור dict , למרות שהגופה של הפונקציה פיתון אשר מגדירה אותו צורכת הפרמטרים במודל כמו model['weight'] לבין model['bias'] . הטיעונים של קריאת batch_loss אינו עבר פשוט לגוף של הפונקציה.

מה קורה כאשר אנו מעלים באוב batch_loss ? גוף Python של batch_loss כבר לייחס ו בהמשכים בתא מעל איפה זה הוגדר. TFF משמש את המתקשר batch_loss בזמן חישוב ההגדרה, וככל היעד של העלאת בזמן batch_loss מופעלת. בשני התפקידים, TFF משמש כגשר בין מערכת הטיפוסים המופשטים של TFF לבין סוגי ייצוג Python. בזמנו השבעה, TFF יקבל סוגים מיכל Python הסטנדרטיים ביותר ( dict , list , tuple , collections.namedtuple , וכו ') כייצוגי בטון של tuples TFF המופשט. כמו כן, למרות שכפי שצוין לעיל, חישובי TFF מקבלים רשמית רק פרמטר בודד, אתה יכול להשתמש בתחביר הקריאה של Python המוכר עם ארגומנטים מיקוםיים ו/או מילות מפתח במקרה שבו סוג הפרמטר הוא tuple - זה עובד כצפוי.

ירידה בשיפוע על אצווה בודדת

כעת, בואו נגדיר חישוב שמשתמש בפונקציית ההפסד הזו כדי לבצע שלב בודד של ירידה בשיפוע. שים לב איך בהגדרת פונקציה זו, אנו משתמשים batch_loss בתור רכיב משנה. ניתן להפעיל חישוב נבנה עם tff.tf_computation בתוך הגוף של חישוב אחר, אם כי בדרך כלל זה לא הכרחי - שכאמור, בגלל בהמשכים מאבדים קצת מידע באגים, הוא לעתים קרובות עדיף עבור חישובים מורכבים יותר לכתוב ולבדוק את כול TensorFlow ללא tff.tf_computation המעצב.

@tff.tf_computation(MODEL_TYPE, BATCH_TYPE, tf.float32)
def batch_train(initial_model, batch, learning_rate):
  # Define a group of model variables and set them to `initial_model`. Must
  # be defined outside the @tf.function.
  model_vars = collections.OrderedDict([
      (name, tf.Variable(name=name, initial_value=value))
      for name, value in initial_model.items()
  ])
  optimizer = tf.keras.optimizers.SGD(learning_rate)

  @tf.function
  def _train_on_batch(model_vars, batch):
    # Perform one step of gradient descent using loss from `batch_loss`.
    with tf.GradientTape() as tape:
      loss = forward_pass(model_vars, batch)
    grads = tape.gradient(loss, model_vars)
    optimizer.apply_gradients(
        zip(tf.nest.flatten(grads), tf.nest.flatten(model_vars)))
    return model_vars

  return _train_on_batch(model_vars, batch)
str(batch_train.type_signature)
'(<initial_model=<weights=float32[784,10],bias=float32[10]>,batch=<x=float32[?,784],y=int32[?]>,learning_rate=float32> -> <weights=float32[784,10],bias=float32[10]>)'

כשאתה להפעיל פונקציה Python מעוטר tff.tf_computation בגוף אחר פונקציה כזו, את ההיגיון של חישוב TFF הפנימי מוטבע (בעצם, inlined) בלוגיקה של אחד החיצוני. כפי שצוין לעיל, אם אתה כותב חישובים שניהם, עדיף צפוי לבצע את הפונקציה הפנימית ( batch_loss במקרה הזה) פייתון רגיל או tf.function ולא tff.tf_computation . עם זאת, כאן אנחנו מדגימים כי קורא אחד tff.tf_computation בתוך אחר בעצם עובד כמצופה. זה עשוי להיות נחוץ אם, למשל, אתה לא צריך את קוד Python המגדיר batch_loss , אבל רק ייצוג TFF בהמשכים שלה.

כעת, בואו נחיל את הפונקציה הזו כמה פעמים על המודל הראשוני כדי לראות אם ההפסד פוחת.

model = initial_model
losses = []
for _ in range(5):
  model = batch_train(model, sample_batch, 0.1)
  losses.append(batch_loss(model, sample_batch))
losses
[0.19690023, 0.13176313, 0.10113225, 0.08273812, 0.070301384]

ירידה בשיפוע על רצף של נתונים מקומיים

עכשיו, מאז batch_train מופיע לעבודה, בואו לכתוב פונקציה הכשרה דומה local_train שצורכת כל רצף של כל אצוות ממשתמש אחד במקום רק אצווה אחת. החישוב החדש יצטרך עכשיו לצרוך tff.SequenceType(BATCH_TYPE) במקום BATCH_TYPE .

LOCAL_DATA_TYPE = tff.SequenceType(BATCH_TYPE)

@tff.federated_computation(MODEL_TYPE, tf.float32, LOCAL_DATA_TYPE)
def local_train(initial_model, learning_rate, all_batches):

  @tff.tf_computation(LOCAL_DATA_TYPE, tf.float32)
  def _insert_learning_rate_to_sequence(dataset, learning_rate):
    return dataset.map(lambda x: (x, learning_rate))

  batches_with_learning_rate = _insert_learning_rate_to_sequence(all_batches, learning_rate)

  # Mapping function to apply to each batch.
  @tff.federated_computation(MODEL_TYPE, batches_with_learning_rate.type_signature.element)
  def batch_fn(model, batch_with_lr):
    batch, lr = batch_with_lr
    return batch_train(model, batch, lr)

  return tff.sequence_reduce(batches_with_learning_rate, initial_model, batch_fn)
str(local_train.type_signature)
'(<initial_model=<weights=float32[784,10],bias=float32[10]>,learning_rate=float32,all_batches=<x=float32[?,784],y=int32[?]>*> -> <weights=float32[784,10],bias=float32[10]>)'

יש לא מעט פרטים קבורים בקטע הקצר הזה של הקוד, בואו נעבור עליהם אחד אחד.

ראשית, בעוד שיכולנו יישמתי ההיגיון הזה כול TensorFlow, בהסתמך על tf.data.Dataset.reduce לעבד את הרצף דומה לאופן שעשינו את זה קודם לכן, שמינו העדיפו הפעם להביע את ההיגיון בשפה הדבקה , בתור tff.federated_computation . השתמשנו המפעיל Federated tff.sequence_reduce לבצע הפחתה.

מפעיל tff.sequence_reduce משמש בדומה tf.data.Dataset.reduce . אתה יכול לחשוב על זה כעל למעשה זהה tf.data.Dataset.reduce , אבל לשימוש בתוך חישובים Federated, אשר כפי שאתה ודאי זוכר, לא יכול להכיל קוד TensorFlow. זהו המפעיל תבנית עם 3-tuple הפרמטר הפורמלי כי מורכב מרצף של T -typed אלמנטים, את המצב ההתחלתי של הפחתה (ואנו מתייחסים אליו באופן מופשט כמו אפס) של כמה סוג U , ואת מפעילת הפחתה של הקלד (<U,T> -> U) שמשנה את מצב ההפחתה ידי עיבוד אלמנט בודד. התוצאה היא המצב הסופי של ההפחתה, לאחר עיבוד כל האלמנטים בסדר רציף. בדוגמה שלנו, מצב ההפחתה הוא המודל שהוכשר על קידומת של הנתונים, והאלמנטים הם קבוצות נתונים.

שנית, פתק שאנחנו שוב השתמשנו חישוב אחד ( batch_train ) כמרכיב בתוך (אחר local_train ), אבל לא באופן ישיר. אנחנו לא יכולים להשתמש בו בתור אופרטור הפחתה כי הוא דורש פרמטר נוסף - קצב הלמידה. כדי לפתור זאת, אנו מגדירים חישוב Federated מוטבע batch_fn הנקשרת local_train של פרמטר learning_rate בגוף שלה. מותר לחישוב צאצא המוגדר כך ללכוד פרמטר פורמלי של ההורה שלו כל עוד חישוב הצאצא לא מופעל מחוץ לגוף ההורה שלו. אתה יכול לחשוב על דפוס זה כמו המקבילה של functools.partial בפייתון.

המשמעות המעשית של לכידת learning_rate בדרך זו היא, כמובן, כי אותו ערך קצב הלמידה משמש בכל קבוצות.

עכשיו, בואו ננסה את פונקצית אימונים המקומית החדש מוגדרת על הרצף השלם של נתונים מאותו המשתמש אשר תרם את המנה המדגמת (ספרה 5 ).

locally_trained_model = local_train(initial_model, 0.1, federated_train_data[5])

האם זה עבד? כדי לענות על שאלה זו, עלינו ליישם הערכה.

הערכה מקומית

הנה דרך אחת ליישם הערכה מקומית על ידי חיבור ההפסדים בכל אצוות הנתונים (יכולנו באותה מידה לחשב את הממוצע; נשאיר זאת כתרגיל לקורא).

@tff.federated_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
def local_eval(model, all_batches):

  @tff.tf_computation(MODEL_TYPE, LOCAL_DATA_TYPE)
  def _insert_model_to_sequence(model, dataset):
    return dataset.map(lambda x: (model, x))

  model_plus_data = _insert_model_to_sequence(model, all_batches)

  @tff.tf_computation(tf.float32, batch_loss.type_signature.result)
  def tff_add(accumulator, arg):
    return accumulator + arg

  return tff.sequence_reduce(
      tff.sequence_map(
          batch_loss,
          model_plus_data), 0., tff_add)
str(local_eval.type_signature)
'(<model=<weights=float32[784,10],bias=float32[10]>,all_batches=<x=float32[?,784],y=int32[?]>*> -> float32)'

שוב, ישנם כמה אלמנטים חדשים המומחשים בקוד זה, בואו נעבור עליהם אחד אחד.

ראשית, השתמשנו בשני מפעילי Federated חדשים עבור רצפי עיבוד: tff.sequence_map שלוקח פונקצית מיפוי T->U ו רצף של T , ופולט רצף של U מתקבל על ידי החלת הפונקציה pointwise המיפוי, ו tff.sequence_sum כי רק מוסיף את כל האלמנטים. כאן, אנו ממפים כל אצווה נתונים לערך הפסד, ולאחר מכן מוסיפים את ערכי ההפסד המתקבלים כדי לחשב את ההפסד הכולל.

הערה שנוכל שוב השתמשתי tff.sequence_reduce , אבל זה לא יהיה הבחירה הטובה ביותר - תהליך ההפחתה הוא, מעצם הגדרתה, רציפים, ואילו מיפוי הסכום ניתן לחשב במקביל. כאשר ניתנת בחירה, עדיף להישאר עם אופרטורים שאינם מגבילים אפשרויות יישום, כך שכאשר חישוב ה-TFF שלנו יערוך בעתיד כדי להיות פריסה לסביבה ספציפית, ניתן לנצל את מלוא ההזדמנויות הפוטנציאליות לביצוע מהיר יותר , ניתן להרחבה יותר, ביצוע חסכוני יותר במשאבים.

שנית, פתק שכשם local_train , הפונקציה המרכיבה שאנחנו צריכים ( batch_loss ) לוקחת פרמטרים יותר ממה מפעיל Federated ( tff.sequence_map ) מצפה, אז אנחנו שוב להגדיר חלקית, מוטבעות הפעם על ידי גלישה ישירות lambda בתור tff.federated_computation . באמצעות עטיפות מוטבעות עם פונקציה כטיעון היא הדרך המומלצת לשימוש tff.tf_computation להטביע TensorFlow הגיון TFF.

עכשיו, בואו נראה אם ​​האימונים שלנו עבדו.

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[5]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[5]))
initial_model loss = 23.025854
locally_trained_model loss = 0.43484688

ואכן, ההפסד ירד. אבל מה קורה אם הערכנו את זה בנתונים של משתמש אחר?

print('initial_model loss =', local_eval(initial_model,
                                         federated_train_data[0]))
print('locally_trained_model loss =',
      local_eval(locally_trained_model, federated_train_data[0]))
initial_model loss = 23.025854
locally_trained_model loss = 74.50075

כצפוי, המצב החמיר. המודל מאומן לזהות 5 , ו מעולם לא ראה 0 . זה מעלה את השאלה - כיצד השפיעה ההכשרה המקומית על איכות המודל מנקודת מבט גלובלית?

הערכה מאוחדת

זו הנקודה במסע שלנו שבה אנו סוף סוף חוזרים לסוגים מאוחדים ולחישובים מאוחדים - הנושא איתו התחלנו. הנה זוג הגדרות מסוג TFF עבור המודל שמקורו בשרת, והנתונים שנותרו על הלקוחות.

SERVER_MODEL_TYPE = tff.type_at_server(MODEL_TYPE)
CLIENT_DATA_TYPE = tff.type_at_clients(LOCAL_DATA_TYPE)

עם כל ההגדרות שהוצגו עד כה, ביטוי של הערכה מאוחדת ב-TFF הוא קו אחד - אנו מפיצים את המודל ללקוחות, נותנים לכל לקוח להפעיל הערכה מקומית על החלק המקומי של הנתונים שלו, ולאחר מכן לבצע ממוצע של ההפסד. הנה דרך אחת לכתוב את זה.

@tff.federated_computation(SERVER_MODEL_TYPE, CLIENT_DATA_TYPE)
def federated_eval(model, data):
  return tff.federated_mean(
      tff.federated_map(local_eval, [tff.federated_broadcast(model),  data]))

כבר ראינו דוגמאות של tff.federated_mean ו tff.federated_map בתרחישים פשוטים, והן ברמת אינטואיטיבית, הם ימשיכו לפעול כרגיל, אך יש עוד בחלק זה של הקוד ממה שנראה לעין, אז בואו נלך על זה בזהירות.

ראשית, הפסקה של לאכזב את האנשים ולתת לכל לקוח Invoke הערכה מקומית על החלק המקומי של חלק נתונים. כזכור מן הסעיפים הקודמים, local_eval יש חתימה סוג של צורה (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

מפעיל Federated tff.federated_map הוא תבנית שמקבלת כפרמטר 2-tuple שמורכבת פונקצית המיפוי של כמה סוגים T->U וערך Federated מהסוג {T}@CLIENTS (כלומר, עם מרכיבי חבר סוג זהה הפרמטר של פונקצית המיפוי), ומחזיר תוצאה מסוג {U}@CLIENTS .

מכיוון שאנו האכלה כן את local_eval כפונקציה מיפוי להחיל על בסיס לכל לקוח, הטיעון השני צריך להיות מסוג Federated {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS , כלומר, את המינוח של הסעיפים הקודמים, שצריך להיות טופל פדרציה. כל לקוח צריך להחזיק סט מלא של טיעונים עבור local_eval בתור consituent חבר. במקום, אנחנו מאכילים אותו Python 2-אלמנט list . מה קורה כאן?

ואכן, זו היא דוגמה יצוק סוג משתמע TFF, בדומה יציקות סוג מרומזת ייתכן נתקלו במקומות אחרים, למשל, כאשר אתה להאכיל int לפונקציה שמקבלת float . ליהוק מרומז משמש כמעט בנקודה זו, אך אנו מתכננים להפוך אותו לנפוץ יותר ב-TFF כדרך למזער את ה-boilerplate.

צוות השחקנים מהרומזים המיושם במקרה זה הוא שקיל בין tuples Federated מהצורה {<X,Y>}@Z , ו tuples של ערכי Federated <{X}@Z,{Y}@Z> . בעוד רשמית, שני אלה הם חתימות מסוג אחר, להסתכל עליה מנקודת המבט של מתכנתים, כל מכשיר Z מחזיקה שתי יחידות של נתונים X ו- Y . מה קורה כאן אינו שונה zip בפייתון, ואכן, אנו מציעים מפעיל tff.federated_zip המאפשר לך לבצע מפורש יש המרות כאלה. כאשר tff.federated_map נתקלת tuple כטענה שנייה, זה פשוט מעורר tff.federated_zip בשבילך.

לאור האמור לעיל, אתה אמור להיות מסוגל לזהות את הביטוי tff.federated_broadcast(model) כמייצג ערך מסוג TFF {MODEL_TYPE}@CLIENTS , וכן data כערך מסוג TFF {LOCAL_DATA_TYPE}@CLIENTS (או פשוט CLIENT_DATA_TYPE ) , מקבל מסונן יחד השניים באמצעות מרומזת tff.federated_zip כדי ליצור את הטיעון השני tff.federated_map .

מפעיל tff.federated_broadcast , כפי שהיית מצפה, פשוט מעביר נתונים מהשרת ללקוחות.

כעת, בואו נראה כיצד האימונים המקומיים שלנו השפיעו על ההפסד הממוצע במערכת.

print('initial_model loss =', federated_eval(initial_model,
                                             federated_train_data))
print('locally_trained_model loss =',
      federated_eval(locally_trained_model, federated_train_data))
initial_model loss = 23.025852
locally_trained_model loss = 54.432625

ואכן, כצפוי, ההפסד גדל. על מנת לשפר את המודל עבור כל המשתמשים, נצטרך להתאמן על הנתונים של כולם.

אימון פדרציה

הדרך הפשוטה ביותר ליישם הכשרה מאוחדת היא להתאמן מקומית, ולאחר מכן לבצע ממוצע של המודלים. זה משתמש באותם אבני בניין ותבניות שכבר דנו בהן, כפי שניתן לראות להלן.

SERVER_FLOAT_TYPE = tff.type_at_server(tf.float32)


@tff.federated_computation(SERVER_MODEL_TYPE, SERVER_FLOAT_TYPE,
                           CLIENT_DATA_TYPE)
def federated_train(model, learning_rate, data):
  return tff.federated_mean(
      tff.federated_map(local_train, [
          tff.federated_broadcast(model),
          tff.federated_broadcast(learning_rate), data
      ]))

שימו לב ביישום המלא מומלץ שקלול Federated שמספק tff.learning , ולא ממוצע של הדגמים, אנו מעדיפים לתותי מודל ממוצעים, עבור מספר הסיבות, למשל, את היכולת קליפ נורמות העדכון עבור דחיסה, וכו .

בואו נראה אם ​​האימון עובד על ידי ריצה של כמה סבבי אימון והשוואת ההפסד הממוצע לפני ואחרי.

model = initial_model
learning_rate = 0.1
for round_num in range(5):
  model = federated_train(model, learning_rate, federated_train_data)
  learning_rate = learning_rate * 0.9
  loss = federated_eval(model, federated_train_data)
  print('round {}, loss={}'.format(round_num, loss))
round 0, loss=21.60552215576172
round 1, loss=20.365678787231445
round 2, loss=19.27480125427246
round 3, loss=18.311111450195312
round 4, loss=17.45725440979004

למען השלמות, בואו נרוץ כעת גם על נתוני הבדיקה כדי לאשר שהמודל שלנו מכליל היטב.

print('initial_model test loss =',
      federated_eval(initial_model, federated_test_data))
print('trained_model test loss =', federated_eval(model, federated_test_data))
initial_model test loss = 22.795593
trained_model test loss = 17.278767

זה מסיים את ההדרכה שלנו.

כמובן, הדוגמה הפשוטה שלנו לא משקפת מספר דברים שתצטרך לעשות בתרחיש ריאליסטי יותר - לדוגמה, לא חישבנו מדדים מלבד הפסד. אנו ממליצים לך ללמוד היישום של מיצוע Federated ב tff.learning כדוגמא שלמה יותר, וגם כדרך להפגין כמה שיטות הקידוד אנחנו רוצים לעודד.