الخوارزميات الموحدة المخصصة ، الجزء 2: تنفيذ المتوسطات الموحدة

عرض على TensorFlow.org تشغيل في Google Colab عرض المصدر على جيثب تحميل دفتر

هذا البرنامج التعليمي هو الجزء الثاني من سلسلة الجزء الثاني الذي يوضح كيفية تنفيذ أنواع مخصصة من الخوارزميات الاتحادية في TFF باستخدام الاتحادية الأساسية (FC) ، والتي هي بمثابة الأساس لل اتحاد التعلم (FL) طبقة ( tff.learning ) .

نحن نشجعك على قراءة لأول مرة الجزء الأول من هذه السلسلة ، الذي يعرض بعض المفاهيم الرئيسية والتجريد البرمجة المستخدمة هنا.

يستخدم الجزء الثاني من السلسلة الآليات التي تم تقديمها في الجزء الأول لتنفيذ نسخة بسيطة من خوارزميات التدريب والتقييم الموحدة.

ونحن نشجعكم على مراجعة تصنيف الصور و توليد النص الدروس عن مستوى أعلى ومقدمة أكثر لطيف لاتحاد واجهات برمجة التطبيقات التعلم 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!'

تنفيذ المتوسطات الموحدة

كما هو الحال في اتحاد التعلم للتصنيف صور ، ونحن نذهب إلى استخدام المثال MNIST، ولكن لأن هذا هو المقصود منها أن تكون تعليمي على مستوى منخفض، ونحن نذهب لتجاوز API Keras و 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 ، على سبيل المثال ، كقائمة من القوائم - القائمة الخارجية التي تمتد عبر المستخدمين (الأرقام) ، والقائمة الداخلية التي تتراوح بين مجموعات من البيانات في تسلسل كل عميل. كما هي العادة، ونحن سوف هيكلة كل دفعة كزوج من التنسورات يدعى x و y ، مع كل البعد دفعة الرائدة. بينما في ذلك، وسنعمل أيضا على تسطيح كل صورة في ناقلات 784 عنصر وإعادة مقياس بكسل في ذلك إلى 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()

بي إن جي

حول الجمع بين TensorFlow و TFF

في هذا البرنامج التعليمي، لالاكتناز نحن على الفور تزيين الوظائف التي تدخل منطق TensorFlow مع tff.tf_computation . ومع ذلك ، بالنسبة للمنطق الأكثر تعقيدًا ، ليس هذا هو النمط الذي نوصي به. يمكن أن يكون تصحيح أخطاء TensorFlow تحديًا بالفعل ، كما أن تصحيح أخطاء TensorFlow بعد إجراء تسلسل كامل ثم إعادة استيراده يفقد بالضرورة بعض البيانات الوصفية ويحد من التفاعل ، مما يجعل تصحيح الأخطاء أكثر صعوبة.

ولذلك، فإننا نوصي بشدة كتابة منطق TF معقدة وظائف بيثون مستقل (وهذا هو، دون tff.tf_computation الديكور). بهذه الطريقة منطق TensorFlow يمكن تطويرها واختبارها باستخدام TF أفضل الممارسات والأدوات (مثل وضع حريصة)، قبل تسلسل حساب لTFF (على سبيل المثال، من خلال التذرع tff.tf_computation مع وظيفة بيثون كما الوسيطة).

تحديد دالة الخسارة

الآن بعد أن أصبح لدينا البيانات ، دعنا نحدد وظيفة الخسارة التي يمكننا استخدامها للتدريب. أولاً ، دعنا نحدد نوع الإدخال على أنه 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 باستخدام بيثون، تحت غطاء محرك السيارة الحسابية TFF ليست بيثون. رمز BATCH_TYPE المحددة أعلاه يمثل مواصفات نوع TFF مجردة. من المهم أن نميز هذا النوع TFF مجردة من أنواع الخرسانة بيثون التمثيل، على سبيل المثال، الحاويات مثل dict أو collections.namedtuple التي يمكن أن تستخدم لتمثيل نوع TFF في الجسم وظيفة بيثون. على عكس بيثون، TFF ديه مجردة نوع منشئ احد tff.StructType لالصفوف (tuple) مثل الحاويات، مع العناصر التي يمكن الكشف عن اسمه بشكل فردي أو اليسار لم يسمها. يستخدم هذا النوع أيضًا لنمذجة المعلمات الرسمية للحسابات ، حيث يمكن لحسابات TFF رسميًا فقط الإعلان عن معلمة واحدة ونتيجة واحدة - سترى أمثلة على ذلك قريبًا.

لقد دعونا الآن تحديد نوع TFF المعلمات نموذج، ومرة أخرى باعتباره TFF اسمه الصفوف (tuple) من الأوزان والتحيز.

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 باستخدام بيثون مثل دلالات كانت على الرغم من داخل 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 تم جمعها معا في 2-الصفوف (tuple) من المعلمات الرسمية؛ يمكنك التعرف على نوع 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 ؟ الجسم بيثون من batch_loss وقد تم بالفعل تتبع وتسلسل في الخلية المذكورة أعلاه حيث تم تعريفه. يعمل TFF كما المتصل إلى batch_loss في الوقت تعريف حساب، وبما أن الهدف من الاحتجاج في الوقت batch_loss يتم استدعاء. في كلا الدورين ، يعمل TFF كجسر بين نظام النوع المجرد الخاص بـ TFF وأنواع تمثيل Python. وفي الوقت الاحتجاج، وTFF استعرض أنواع بيثون الحاويات الأكثر القياسية ( dict ، list ، tuple ، collections.namedtuple ، الخ)، والتمثيل ملموسة من المجموعات 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]>)'

عند استدعاء وظيفة بيثون مزينة tff.tf_computation داخل الجسم هذه وظيفة أخرى، وجزءا لا يتجزأ من منطق حساب TFF الداخلي (أساسا، inlined) في منطق واحد الخارجي. كما ذكر أعلاه، إذا كنت تكتب على حد سواء الحسابية، فمن المرجح الأفضل لجعل وظيفة الداخلية ( batch_loss في هذه الحالة) بايثون العادية أو tf.function بدلا من tff.tf_computation . ومع ذلك، ونحن هنا توضيح أن استدعاء أحد tff.tf_computation داخل آخر يعمل في الأساس كما هو متوقع. قد يكون هذا ضروريا إذا، على سبيل المثال، لم يكن لديك رمز بيثون تحديد 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 . لقد استخدمنا المشغل متحدة tff.sequence_reduce لأداء الحد.

المشغل tff.sequence_reduce يستخدم على نحو مماثل ل tf.data.Dataset.reduce . يمكنك التفكير في الأمر على النحو أساسا نفس tf.data.Dataset.reduce ، ولكن للاستخدام داخل الحسابية الاتحادية، والتي كما تذكرون، لا يمكن أن تحتوي على شفرة TensorFlow. وهو مشغل القالب مع معلمة رسمية 3-الصفوف (tuple) التي تتكون من سلسلة من T -typed العناصر، الدولة الأولى من التخفيض (نحن سوف أشير إليها تجريدي صفر) من نوع U ، والمشغل للحد من اكتب (<U,T> -> U) أن يغير حالة انخفاض من خلال تجهيز عنصر واحد. والنتيجة هي الحالة النهائية للتخفيض ، بعد معالجة جميع العناصر بترتيب تسلسلي. في مثالنا ، حالة التخفيض هي النموذج المُدرَّب على بادئة البيانات ، والعناصر عبارة عن دفعات بيانات.

الملاحظة الثانية، أن لدينا استخدامها مرة أخرى حساب واحد ( batch_train ) كمكون ضمن آخر ( local_train )، ولكن ليس بشكل مباشر. لا يمكننا استخدامه كعامل تخفيض لأنه يتطلب معلمة إضافية - معدل التعلم. لحل هذه المشكلة، نحن تعريف متحدة حساب جزءا لا يتجزأ من 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)'

مرة أخرى ، هناك بعض العناصر الجديدة الموضحة في هذا الكود ، دعنا نتناولها واحدة تلو الأخرى.

أولا، لقد استخدمت شركتي الاتحادية جديدة لمعالجة متواليات: tff.sequence_map أن يأخذ وظيفة رسم الخرائط T->U وسلسلة من T ، وتنبعث سلسلة من U الحصول عليها عن طريق تطبيق الخرائط وظيفة pointwise، و tff.sequence_sum أن فقط يضيف كل العناصر. هنا ، نقوم بتعيين كل دفعة بيانات إلى قيمة خسارة ، ثم نضيف قيم الخسارة الناتجة لحساب الخسارة الإجمالية.

ملاحظة أننا يمكن أن تستخدم مرة أخرى tff.sequence_reduce ، ولكن هذا لن يكون أفضل خيار - عملية الخفض، بحكم التعريف، متتابعة، في حين يمكن حسابها التعيين ومبلغ بالتوازي. عند منحك خيارًا ، من الأفضل الالتزام بالمشغلين الذين لا يقيدون خيارات التنفيذ ، بحيث عندما يتم تجميع حساب TFF الخاص بنا في المستقبل لنشره في بيئة معينة ، يمكن للمرء الاستفادة الكاملة من جميع الفرص المحتملة للحصول على أسرع ، أكثر قابلية للتطوير ، وتنفيذ أكثر كفاءة في استخدام الموارد.

ثانيا، علما بأن تماما كما في local_train ، وظيفة المكونة نحتاج ( batch_loss ) يأخذ أكثر من المعلمات مما المشغل الاتحادية ( 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 في سيناريوهات أبسط، وعلى مستوى بديهية، فإنها تعمل كما هو متوقع، ولكن هناك المزيد في هذا القسم من التعليمات البرمجية مما تراه العين، لذلك دعونا نذهب أكثر من ذلك بعناية.

أولا، دعونا كسر أسفل يسمح لكل عميل استدعاء والتقييم المحلي على الجزء المحلي في جزء البيانات. كما تذكرون من الأقسام السابقة، local_eval لديه توقيع نوع من النموذج (<MODEL_TYPE, LOCAL_DATA_TYPE> -> float32) .

المشغل متحدة tff.federated_map هو القالب الذي يقبل كمعلمة 2-الصفوف (tuple) التي تتكون من وظيفة رسم الخرائط من نوع T->U وقيمة الاتحادية من نوع {T}@CLIENTS (أي مع الناخبين عضو نفس نوع المعلمة وظيفة رسم الخرائط)، والعوائد نتيجة لنوع {U}@CLIENTS .

وبما أننا التغذية local_eval بوصفها وظيفة رسم الخرائط أن يطبق على أساس لكل عميل، يجب أن تكون الوسيطة الثانية من نوع الاتحادية {<MODEL_TYPE, LOCAL_DATA_TYPE>}@CLIENTS ، أي في تسمية الأجزاء السابقة، فإنه ينبغي كن مجموعة موحدة. يجب على كل عميل عقد مجموعة كاملة من الحجج ل local_eval باعتباره consituent الأعضاء. بدلا من ذلك، نحن تغذية عليه 2-عنصر بيثون list . ماذا يحصل هنا؟

في الواقع، وهذا هو مثال لنوع يلقي ضمنا في TFF، على غرار يلقي نوع الضمني قد واجهت في أي مكان آخر، على سبيل المثال، عند إطعام int إلى وظيفة يقبل float . نادرًا ما يتم استخدام الصب الضمني في هذه المرحلة ، لكننا نخطط لجعله أكثر انتشارًا في TFF كطريقة لتقليل النموذج المعياري.

المدلى بها الضمني الذي تم تطبيقه في هذه الحالة هو التكافؤ بين الصفوف الاتحادية النموذج {<X,Y>}@Z ، والصفوف من اتحاد القيم <{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
      ]))

علما بأن في تنفيذ كامل المواصفات من المتوسط الاتحادية التي تقدمها 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

بهذا نختتم درسنا التعليمي.

بالطبع ، لا يعكس مثالنا المبسط عددًا من الأشياء التي تحتاج إلى القيام بها في سيناريو أكثر واقعية - على سبيل المثال ، لم نحسب مقاييس بخلاف الخسارة. نحن نشجعكم على دراسة تنفيذ من المتوسط الاتحادية في tff.learning كمثال أكثر اكتمالا، وكوسيلة لإظهار بعض الممارسات ترميز نحن نرغب في تشجيع.