![]() |
![]() |
![]() |
![]() |
本教學課程探討部分本機聯邦學習,其中部分用戶端參數永遠不會在伺服器上彙總。這適用於具有使用者特定參數的模型 (例如矩陣分解模型) 以及在通訊受限設定中進行訓練。我們以用於影像分類的聯邦學習教學課程中介紹的概念為基礎;如同該教學課程,我們在 tff.learning
中介紹高階 API,用於聯邦訓練和評估。
首先,我們闡述部分本機聯邦學習的動機,用於 矩陣分解。我們描述聯邦重建 (論文、部落格文章),這是一種適用於大規模部分本機聯邦學習的實用演算法。我們準備 MovieLens 1M 資料集,建構部分本機模型,並訓練和評估該模型。
!pip install --quiet --upgrade tensorflow-federated
!pip install --quiet --upgrade nest-asyncio
import nest_asyncio
nest_asyncio.apply()
import collections
import functools
import io
import os
import requests
import zipfile
from typing import List, Optional, Tuple
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_federated as tff
np.random.seed(42)
背景:矩陣分解
矩陣分解一直以來都是熱門技術,可根據使用者互動學習推薦和項目內嵌表示法。典型的範例是電影推薦,其中有 \(n\) 位使用者和 \(m\) 部電影,而使用者已對某些電影評分。給定一位使用者,我們會使用其評分歷史記錄和類似使用者的評分來預測使用者對他們未看過的電影的評分。如果我們有可以預測評分的模型,就很容易向使用者推薦他們會喜歡的新電影。
對於此任務,將使用者評分表示為 \(n \times m\) 矩陣 \(R\) 會很有用
此矩陣通常是稀疏的,因為使用者通常只會看到資料集中一小部分的電影。矩陣分解的輸出是兩個矩陣:\(n \times k\) 矩陣 \(U\),表示每位使用者的 \(k\) 維使用者內嵌,以及 \(m \times k\) 矩陣 \(I\),表示每個項目的 \(k\) 維項目內嵌。最簡單的訓練目標是確保使用者和項目內嵌的點積可預測觀察到的評分 \(O\)
\[argmin_{U,I} \sum_{(u, i) \in O} (R_{ui} - U_u I_i^T)^2\]
這相當於最小化觀察到的評分與透過取得對應的使用者和項目內嵌的點積所預測的評分之間的均方誤差。另一種解讀方式是,這可確保已知評分的 \(R \approx UI^T\),因此稱為「矩陣分解」。如果這令人困惑,請別擔心,我們不需要知道矩陣分解的細節即可完成本教學課程的其餘部分。
探索 MovieLens 資料
首先載入 MovieLens 1M 資料,其中包含來自 6040 位使用者對 3706 部電影的 1,000,209 個電影評分。
def download_movielens_data(dataset_path):
"""Downloads and copies MovieLens data to local /tmp directory."""
if dataset_path.startswith('http'):
r = requests.get(dataset_path)
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall(path='/tmp')
else:
tf.io.gfile.makedirs('/tmp/ml-1m/')
for filename in ['ratings.dat', 'movies.dat', 'users.dat']:
tf.io.gfile.copy(
os.path.join(dataset_path, filename),
os.path.join('/tmp/ml-1m/', filename),
overwrite=True)
download_movielens_data('http://files.grouplens.org/datasets/movielens/ml-1m.zip')
def load_movielens_data(
data_directory: str = "/tmp",
) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Loads pandas DataFrames for ratings, movies, users from data directory."""
# Load pandas DataFrames from data directory. Assuming data is formatted as
# specified in http://files.grouplens.org/datasets/movielens/ml-1m-README.txt.
ratings_df = pd.read_csv(
os.path.join(data_directory, "ml-1m", "ratings.dat"),
sep="::",
names=["UserID", "MovieID", "Rating", "Timestamp"], engine="python")
movies_df = pd.read_csv(
os.path.join(data_directory, "ml-1m", "movies.dat"),
sep="::",
names=["MovieID", "Title", "Genres"], engine="python",
encoding = "ISO-8859-1")
# Create dictionaries mapping from old IDs to new (remapped) IDs for both
# MovieID and UserID. Use the movies and users present in ratings_df to
# determine the mapping, since movies and users without ratings are unneeded.
movie_mapping = {
old_movie: new_movie for new_movie, old_movie in enumerate(
ratings_df.MovieID.astype("category").cat.categories)
}
user_mapping = {
old_user: new_user for new_user, old_user in enumerate(
ratings_df.UserID.astype("category").cat.categories)
}
# Map each DataFrame consistently using the now-fixed mapping.
ratings_df.MovieID = ratings_df.MovieID.map(movie_mapping)
ratings_df.UserID = ratings_df.UserID.map(user_mapping)
movies_df.MovieID = movies_df.MovieID.map(movie_mapping)
# Remove nulls resulting from some movies being in movies_df but not
# ratings_df.
movies_df = movies_df[pd.notnull(movies_df.MovieID)]
return ratings_df, movies_df
讓我們載入並探索幾個包含評分和電影資料的 Pandas DataFrame。
ratings_df, movies_df = load_movielens_data()
我們可以看到每個評分範例都有 1-5 的評分、對應的 UserID、對應的 MovieID 和時間戳記。
ratings_df.head()
每部電影都有片名,而且可能有多種類型。
movies_df.head()
瞭解資料集的基本統計資料始終是個好主意
print('Num users:', len(set(ratings_df.UserID)))
print('Num movies:', len(set(ratings_df.MovieID)))
Num users: 6040 Num movies: 3706
ratings = ratings_df.Rating.tolist()
plt.hist(ratings, bins=5)
plt.xticks([1, 2, 3, 4, 5])
plt.ylabel('Count')
plt.xlabel('Rating')
plt.show()
print('Average rating:', np.mean(ratings))
print('Median rating:', np.median(ratings))
Average rating: 3.581564453029317 Median rating: 4.0
我們也可以繪製最熱門的電影類型。
movie_genres_list = movies_df.Genres.tolist()
# Count the number of times each genre describes a movie.
genre_count = collections.defaultdict(int)
for genres in movie_genres_list:
curr_genres_list = genres.split('|')
for genre in curr_genres_list:
genre_count[genre] += 1
genre_name_list, genre_count_list = zip(*genre_count.items())
plt.figure(figsize=(11, 11))
plt.pie(genre_count_list, labels=genre_name_list)
plt.title('MovieLens Movie Genres')
plt.show()
此資料自然劃分為來自不同使用者的評分,因此我們預期用戶端之間存在一些資料異質性。以下我們顯示不同使用者最常評分的電影類型。我們可以觀察到使用者之間存在顯著差異。
def print_top_genres_for_user(ratings_df, movies_df, user_id):
"""Prints top movie genres for user with ID user_id."""
user_ratings_df = ratings_df[ratings_df.UserID == user_id]
movie_ids = user_ratings_df.MovieID
genre_count = collections.Counter()
for movie_id in movie_ids:
genres_string = movies_df[movies_df.MovieID == movie_id].Genres.tolist()[0]
for genre in genres_string.split('|'):
genre_count[genre] += 1
print(f'\nFor user {user_id}:')
for (genre, freq) in genre_count.most_common(5):
print(f'{genre} was rated {freq} times')
print_top_genres_for_user(ratings_df, movies_df, user_id=0)
print_top_genres_for_user(ratings_df, movies_df, user_id=10)
print_top_genres_for_user(ratings_df, movies_df, user_id=19)
For user 0: Drama was rated 21 times Children's was rated 20 times Animation was rated 18 times Musical was rated 14 times Comedy was rated 14 times For user 10: Comedy was rated 84 times Drama was rated 54 times Romance was rated 22 times Thriller was rated 18 times Action was rated 9 times For user 19: Action was rated 17 times Sci-Fi was rated 9 times Thriller was rated 9 times Drama was rated 6 times Crime was rated 5 times
預先處理 MovieLens 資料
我們現在將準備 MovieLens 資料集,作為 tf.data.Dataset
清單,代表每個使用者的資料,以便與 TFF 搭配使用。
我們實作兩個函式
create_tf_datasets
:採用我們的評分 DataFrame 並產生使用者分割的tf.data.Dataset
清單。split_tf_datasets
:採用資料集清單,並依使用者將其分割為訓練/驗證/測試集,因此驗證/測試集僅包含來自訓練期間未見過的使用者的評分。通常在標準集中式矩陣分解中,我們實際上會進行分割,以便驗證/測試集包含來自見過的使用者的保留評分,因為未見過的使用者沒有使用者內嵌。在我們的案例中,稍後我們將看到,我們用來在 FL 中啟用矩陣分解的方法也支援快速重建未見過的使用者的使用者內嵌。
def create_tf_datasets(ratings_df: pd.DataFrame,
batch_size: int = 1,
max_examples_per_user: Optional[int] = None,
max_clients: Optional[int] = None) -> List[tf.data.Dataset]:
"""Creates TF Datasets containing the movies and ratings for all users."""
num_users = len(set(ratings_df.UserID))
# Optionally limit to `max_clients` to speed up data loading.
if max_clients is not None:
num_users = min(num_users, max_clients)
def rating_batch_map_fn(rating_batch):
"""Maps a rating batch to an OrderedDict with tensor values."""
# Each example looks like: {x: movie_id, y: rating}.
# We won't need the UserID since each client will only look at their own
# data.
return collections.OrderedDict([
("x", tf.cast(rating_batch[:, 1:2], tf.int64)),
("y", tf.cast(rating_batch[:, 2:3], tf.float32))
])
tf_datasets = []
for user_id in range(num_users):
# Get subset of ratings_df belonging to a particular user.
user_ratings_df = ratings_df[ratings_df.UserID == user_id]
tf_dataset = tf.data.Dataset.from_tensor_slices(user_ratings_df)
# Define preprocessing operations.
tf_dataset = tf_dataset.take(max_examples_per_user).shuffle(
buffer_size=max_examples_per_user, seed=42).batch(batch_size).map(
rating_batch_map_fn,
num_parallel_calls=tf.data.experimental.AUTOTUNE)
tf_datasets.append(tf_dataset)
return tf_datasets
def split_tf_datasets(
tf_datasets: List[tf.data.Dataset],
train_fraction: float = 0.8,
val_fraction: float = 0.1,
) -> Tuple[List[tf.data.Dataset], List[tf.data.Dataset], List[tf.data.Dataset]]:
"""Splits a list of user TF datasets into train/val/test by user.
"""
np.random.seed(42)
np.random.shuffle(tf_datasets)
train_idx = int(len(tf_datasets) * train_fraction)
val_idx = int(len(tf_datasets) * (train_fraction + val_fraction))
# Note that the val and test data contains completely different users, not
# just unseen ratings from train users.
return (tf_datasets[:train_idx], tf_datasets[train_idx:val_idx],
tf_datasets[val_idx:])
# We limit the number of clients to speed up dataset creation. Feel free to pass
# max_clients=None to load all clients' data.
tf_datasets = create_tf_datasets(
ratings_df=ratings_df,
batch_size=5,
max_examples_per_user=300,
max_clients=2000)
# Split the ratings into training/val/test by client.
tf_train_datasets, tf_val_datasets, tf_test_datasets = split_tf_datasets(
tf_datasets,
train_fraction=0.8,
val_fraction=0.1)
作為快速檢查,我們可以列印一批訓練資料。我們可以看到每個個別範例都包含「x」鍵下的 MovieID 和「y」鍵下的評分。請注意,我們不需要 UserID,因為每位使用者只會看到自己的資料。
print(next(iter(tf_train_datasets[0])))
OrderedDict([('x', <tf.Tensor: shape=(5, 1), dtype=int64, numpy= array([[1907], [2891], [1574], [2785], [2775]])>), ('y', <tf.Tensor: shape=(5, 1), dtype=float32, numpy= array([[3.], [3.], [3.], [4.], [3.]], dtype=float32)>)])
我們可以繪製長條圖,顯示每位使用者的評分數量。
def count_examples(curr_count, batch):
return curr_count + tf.size(batch['x'])
num_examples_list = []
# Compute number of examples for every other user.
for i in range(0, len(tf_train_datasets), 2):
num_examples = tf_train_datasets[i].reduce(tf.constant(0), count_examples).numpy()
num_examples_list.append(num_examples)
plt.hist(num_examples_list, bins=10)
plt.ylabel('Count')
plt.xlabel('Number of Examples')
plt.show()
現在我們已載入並探索資料,我們將討論如何將矩陣分解引入聯邦學習。在此過程中,我們將闡述部分本機聯邦學習的動機。
將矩陣分解引入 FL
雖然矩陣分解傳統上用於集中式設定中,但它在聯邦學習中尤其相關:使用者評分可能存在於個別用戶端裝置上,而且我們可能希望在不集中資料的情況下學習使用者和項目的內嵌和推薦。由於每位使用者都有對應的使用者內嵌,因此讓每個用戶端儲存其使用者內嵌是很自然的,這比中央伺服器儲存所有使用者內嵌更具擴充性。
以下是將矩陣分解引入 FL 的一項建議
- 伺服器儲存項目矩陣 \(I\) 並在每一輪將其傳送給取樣的用戶端
- 用戶端使用上述目標上的 SGD 更新項目矩陣及其個人使用者內嵌 \(U_u\)
- 對 \(I\) 的更新會在伺服器上彙總,更新下一輪的 \(I\) 伺服器副本
此方法是部分本機,也就是說,某些用戶端參數永遠不會由伺服器彙總。雖然此方法很吸引人,但它要求用戶端在各輪之間維護狀態,即其使用者內嵌。具狀態聯邦演算法不太適合跨裝置 FL 設定:在這些設定中,母體大小通常遠大於每一輪中參與的用戶端數量,而且用戶端通常在訓練過程中最多參與一次。除了依賴可能未初始化的狀態之外,當用戶端不常取樣時,具狀態演算法可能會因狀態過時而導致效能降低。重要的是,在矩陣分解設定中,具狀態演算法會導致所有未見過的使用者都遺失訓練的使用者內嵌,而且在大型訓練中,大多數使用者可能都未見過。如需更多關於跨裝置 FL 中無狀態演算法動機的資訊,請參閱 Wang 等人,2021 年第 3.1.1 節 和 Reddi 等人,2020 年第 5.1 節。
聯邦重建 (Singhal 等人,2021 年) 是上述方法的無狀態替代方案。關鍵概念是,用戶端不是跨輪儲存使用者內嵌,而是在需要時重建使用者內嵌。當 FedRecon 應用於矩陣分解時,訓練會依循以下步驟進行
- 伺服器儲存項目矩陣 \(I\) 並在每一輪將其傳送給取樣的用戶端
- 每個用戶端凍結 \(I\) 並使用一或多個 SGD 步驟訓練其使用者內嵌 \(U_u\) (重建)
- 每個用戶端凍結 \(U_u\) 並使用一或多個 SGD 步驟訓練 \(I\)
- 對 \(I\) 的更新會在使用者之間彙總,更新下一輪的 \(I\) 伺服器副本
此方法不需要用戶端在各輪之間維護狀態。作者也在論文中顯示,此方法可快速重建未見過用戶端的使用者內嵌 (第 4.2 節、圖 3 和表 1),讓大多數未參與訓練的用戶端都能擁有訓練模型,從而為這些用戶端啟用推薦。請參閱聯邦重建 Google AI 部落格文章,以瞭解更多主要結果。
定義模型
接下來,我們將定義要在用戶端裝置上訓練的本機矩陣分解模型。此模型將包含完整的項目矩陣 \(I\) 和單一使用者用戶端 \(u\) 的內嵌 \(U_u\)。請注意,用戶端不需要儲存完整的使用者矩陣 \(U\)。
我們將定義以下內容
UserEmbedding
:一個簡單的 Keras 層,代表單一num_latent_factors
維使用者內嵌。get_matrix_factorization_model
:一個函式,會傳回tff.learning.models.ReconstructionModel
,其中包含模型邏輯,包括哪些層級在全球伺服器上彙總,以及哪些層級保持在本機。我們需要此額外資訊才能初始化聯邦重建訓練程序。在這裡,我們使用tff.learning.models.ReconstructionModel.from_keras_model_and_layers
從 Keras 模型產生tff.learning.models.ReconstructionModel
。與tff.learning.models.VariableModel
類似,我們也可以透過實作類別介面來實作自訂tff.learning.models.ReconstructionModel
。
class UserEmbedding(tf.keras.layers.Layer):
"""Keras layer representing an embedding for a single user, used below."""
def __init__(self, num_latent_factors, **kwargs):
super().__init__(**kwargs)
self.num_latent_factors = num_latent_factors
def build(self, input_shape):
self.embedding = self.add_weight(
shape=(1, self.num_latent_factors),
initializer='uniform',
dtype=tf.float32,
name='UserEmbeddingKernel')
super().build(input_shape)
def call(self, inputs):
return self.embedding
def compute_output_shape(self):
return (1, self.num_latent_factors)
def get_matrix_factorization_model(
num_items: int,
num_latent_factors: int) -> tff.learning.models.ReconstructionModel:
"""Defines a Keras matrix factorization model."""
# Layers with variables will be partitioned into global and local layers.
# We'll pass this to `tff.learning.models.ReconstructionModel.from_keras_model_and_layers`.
global_layers = []
local_layers = []
# Extract the item embedding.
item_input = tf.keras.layers.Input(shape=[1], name='Item')
item_embedding_layer = tf.keras.layers.Embedding(
num_items,
num_latent_factors,
name='ItemEmbedding')
global_layers.append(item_embedding_layer)
flat_item_vec = tf.keras.layers.Flatten(name='FlattenItems')(
item_embedding_layer(item_input))
# Extract the user embedding.
user_embedding_layer = UserEmbedding(
num_latent_factors,
name='UserEmbedding')
local_layers.append(user_embedding_layer)
# The item_input never gets used by the user embedding layer,
# but this allows the model to directly use the user embedding.
flat_user_vec = user_embedding_layer(item_input)
# Compute the dot product between the user embedding, and the item one.
pred = tf.keras.layers.Dot(
1, normalize=False, name='Dot')([flat_user_vec, flat_item_vec])
input_spec = collections.OrderedDict(
x=tf.TensorSpec(shape=[None, 1], dtype=tf.int64),
y=tf.TensorSpec(shape=[None, 1], dtype=tf.float32))
model = tf.keras.Model(inputs=item_input, outputs=pred)
return tff.learning.models.ReconstructionModel.from_keras_model_and_layers(
keras_model=model,
global_layers=global_layers,
local_layers=local_layers,
input_spec=input_spec)
類似於聯邦平均的介面,聯邦重建的介面預期會有一個沒有引數的 model_fn
,會傳回 tff.learning.models.ReconstructionModel
。
# This will be used to produce our training process.
# User and item embeddings will be 50-dimensional.
model_fn = functools.partial(
get_matrix_factorization_model,
num_items=3706,
num_latent_factors=50)
接下來,我們將定義 loss_fn
和 metrics_fn
,其中 loss_fn
是一個沒有引數的函式,會傳回要用於訓練模型的 Keras 損失,而 metrics_fn
是一個沒有引數的函式,會傳回用於評估的 Keras 指標清單。這些是建構訓練和評估運算所需的。
我們將使用均方誤差作為損失,如上所述。對於評估,我們將使用評分準確度 (當模型的預測點積四捨五入到最接近的整數時,它與標籤評分相符的頻率?)。
class RatingAccuracy(tf.keras.metrics.Mean):
"""Keras metric computing accuracy of reconstructed ratings."""
def __init__(self,
name: str = 'rating_accuracy',
**kwargs):
super().__init__(name=name, **kwargs)
def update_state(self,
y_true: tf.Tensor,
y_pred: tf.Tensor,
sample_weight: Optional[tf.Tensor] = None):
absolute_diffs = tf.abs(y_true - y_pred)
# A [batch_size, 1] tf.bool tensor indicating correctness within the
# threshold for each example in a batch. A 0.5 threshold corresponds
# to correctness when predictions are rounded to the nearest whole
# number.
example_accuracies = tf.less_equal(absolute_diffs, 0.5)
super().update_state(example_accuracies, sample_weight=sample_weight)
loss_fn = lambda: tf.keras.losses.MeanSquaredError()
metrics_fn = lambda: [RatingAccuracy()]
訓練和評估
現在我們擁有定義訓練程序所需的一切。與 聯邦平均的介面 的一個重要區別在於,我們現在傳入 reconstruction_optimizer_fn
,它將在重建本機參數 (在我們的案例中,使用者內嵌) 時使用。在這裡使用 SGD
通常是合理的,其學習率與用戶端最佳化工具學習率相似或稍低。我們在下方提供可行的配置。這尚未仔細調整,因此請隨意嘗試不同的值。
查看文件以瞭解更多詳細資訊和選項。
# We'll use this by doing:
# state = training_process.initialize()
# state, metrics = training_process.next(state, federated_train_data)
training_process = tff.learning.algorithms.build_fed_recon(
model_fn=model_fn,
loss_fn=loss_fn,
metrics_fn=metrics_fn,
server_optimizer_fn=lambda: tf.keras.optimizers.SGD(1.0),
client_optimizer_fn=lambda: tf.keras.optimizers.SGD(0.5),
reconstruction_optimizer_fn=lambda: tf.keras.optimizers.SGD(0.1))
我們也可以定義一個運算來評估我們訓練的全球模型。
evaluation_process = tff.learning.algorithms.build_fed_recon_eval(
model_fn,
loss_fn=loss_fn,
metrics_fn=metrics_fn,
reconstruction_optimizer_fn=functools.partial(
tf.keras.optimizers.SGD, 0.1))
我們可以初始化訓練程序狀態並檢查它。最重要的是,我們可以看見此伺服器狀態僅儲存項目變數 (目前隨機初始化),而不儲存任何使用者內嵌。
state = training_process.initialize()
model = training_process.get_model_weights(state)
print(model)
print('Item variables shape:', model.trainable[0].shape)
ModelWeights(trainable=[array([[-0.01839826, 0.04044249, -0.04871846, ..., 0.01967763, -0.03034571, -0.01698984], [-0.03716197, 0.0374358 , 0.00968184, ..., -0.04857936, -0.0385102 , -0.01883975], [-0.01227728, -0.04690691, 0.00250578, ..., 0.01141983, 0.01773251, 0.03525344], ..., [ 0.03374172, 0.02467764, 0.00621947, ..., -0.01521915, -0.01185555, 0.0295455 ], [-0.04029766, -0.02826073, 0.0358924 , ..., -0.02519268, -0.03909808, -0.01965014], [-0.04007702, -0.04353172, 0.04063287, ..., 0.01851353, -0.00767929, -0.00816654]], dtype=float32)], non_trainable=[]) Item variables shape: (3706, 50)
我們也可以嘗試在驗證用戶端上評估我們的隨機初始化模型。此處的聯邦重建評估涉及以下步驟
- 伺服器將項目矩陣 \(I\) 傳送給取樣的評估用戶端
- 每個用戶端凍結 \(I\) 並使用一或多個 SGD 步驟訓練其使用者內嵌 \(U_u\) (重建)
- 每個用戶端使用伺服器 \(I\) 和重建的 \(U_u\) 在其本機資料的未見部分計算損失和指標
- 損失和指標會在使用者之間平均,以計算整體損失和指標
請注意,步驟 1 和 2 與訓練相同。這種關聯很重要,因為以與評估相同的方式進行訓練會產生一種元學習或學習如何學習的形式。在此案例中,模型正在學習如何學習全球變數 (項目矩陣),這會導致本機變數 (使用者內嵌) 的高效重建。如需更多資訊,請參閱論文的 第 4.2 節。
步驟 2 和 3 也必須使用用戶端本機資料的不相交部分執行,以確保公平評估,這一點也很重要。根據預設,訓練程序和評估運算都會使用每隔一個範例進行重建,並使用另一半進行重建後處理。可以使用 dataset_split_fn
引數自訂此行為 (稍後我們將進一步探索這一點)。
# We shouldn't expect good evaluation results here, since we haven't trained
# yet!
eval_state = evaluation_process.initialize()
eval_state = evaluation_process.set_model_weights(
eval_state, training_process.get_model_weights(state)
)
_, eval_metrics = evaluation_process.next(eval_state, tf_val_datasets)
print('Initial Eval:', eval_metrics['client_work']['eval'])
Initial Eval: OrderedDict([('rating_accuracy', 0.0), ('loss', 14.365454)])
接下來,我們可以嘗試執行一輪訓練。為了讓事情更真實,我們將在每一輪隨機取樣 50 個用戶端,且不重複取樣。我們仍然應該預期訓練指標會很差,因為我們只進行一輪訓練。
federated_train_data = np.random.choice(tf_train_datasets, size=50, replace=False).tolist()
state, metrics = training_process.next(state, federated_train_data)
print(f'Train metrics:', metrics['client_work']['train'])
Train metrics: OrderedDict([('rating_accuracy', 0.0), ('loss', 14.183293)])
現在讓我們設定一個訓練迴圈,以在多輪中進行訓練。
NUM_ROUNDS = 20
train_losses = []
train_accs = []
state = training_process.initialize()
# This may take a couple minutes to run.
for i in range(NUM_ROUNDS):
federated_train_data = np.random.choice(tf_train_datasets, size=50, replace=False).tolist()
state, metrics = training_process.next(state, federated_train_data)
print(f'Train round {i}:', metrics['client_work']['train'])
train_losses.append(metrics['client_work']['train']['loss'])
train_accs.append(metrics['client_work']['train']['rating_accuracy'])
eval_state = evaluation_process.set_model_weights(
eval_state, training_process.get_model_weights(state))
_, eval_metrics = evaluation_process.next(eval_state, tf_val_datasets)
print('Final Eval:', eval_metrics['client_work']['eval'])
Train round 0: OrderedDict([('rating_accuracy', 0.0), ('loss', 14.523704)]) Train round 1: OrderedDict([('rating_accuracy', 0.0), ('loss', 14.552873)]) Train round 2: OrderedDict([('rating_accuracy', 0.0), ('loss', 14.480412)]) Train round 3: OrderedDict([('rating_accuracy', 0.0051107327), ('loss', 12.155375)]) Train round 4: OrderedDict([('rating_accuracy', 0.042440318), ('loss', 9.201913)]) Train round 5: OrderedDict([('rating_accuracy', 0.11840491), ('loss', 5.5969186)]) Train round 6: OrderedDict([('rating_accuracy', 0.12890044), ('loss', 5.5303264)]) Train round 7: OrderedDict([('rating_accuracy', 0.19774501), ('loss', 3.9932375)]) Train round 8: OrderedDict([('rating_accuracy', 0.21234067), ('loss', 3.5070496)]) Train round 9: OrderedDict([('rating_accuracy', 0.21757619), ('loss', 3.5754187)]) Train round 10: OrderedDict([('rating_accuracy', 0.24020319), ('loss', 3.0558898)]) Train round 11: OrderedDict([('rating_accuracy', 0.2337753), ('loss', 3.1659348)]) Train round 12: OrderedDict([('rating_accuracy', 0.2638889), ('loss', 2.413888)]) Train round 13: OrderedDict([('rating_accuracy', 0.2622365), ('loss', 2.760038)]) Train round 14: OrderedDict([('rating_accuracy', 0.27820238), ('loss', 2.195349)]) Train round 15: OrderedDict([('rating_accuracy', 0.29124364), ('loss', 2.447856)]) Train round 16: OrderedDict([('rating_accuracy', 0.30438596), ('loss', 2.096729)]) Train round 17: OrderedDict([('rating_accuracy', 0.29557413), ('loss', 2.0750825)]) Train round 18: OrderedDict([('rating_accuracy', 0.31832394), ('loss', 1.99085)]) Train round 19: OrderedDict([('rating_accuracy', 0.3162333), ('loss', 2.0302613)]) Final Eval: OrderedDict([('rating_accuracy', 0.3126193), ('loss', 2.0305126)])
我們可以繪製訓練損失和準確度與輪數的關係圖。本筆記本中的超參數尚未仔細調整,因此請隨意嘗試不同的每輪用戶端數、學習率、輪數和用戶端總數,以改善這些結果。
plt.plot(range(NUM_ROUNDS), train_losses)
plt.ylabel('Train Loss')
plt.xlabel('Round')
plt.title('Train Loss')
plt.show()
plt.plot(range(NUM_ROUNDS), train_accs)
plt.ylabel('Train Accuracy')
plt.xlabel('Round')
plt.title('Train Accuracy')
plt.show()
最後,當我們完成調整時,我們可以計算未見測試集上的指標。
eval_state = evaluation_process.set_model_weights(
eval_state, training_process.get_model_weights(state)
)
_, eval_metrics = evaluation_process.next(eval_state, tf_test_datasets)
print('Final Test:', eval_metrics['client_work']['eval'])
Final Test: OrderedDict([('rating_accuracy', 0.3129535), ('loss', 1.9429641)])
進一步探索
做得好,您已完成本筆記本。我們建議以下練習,以進一步探索部分本機聯邦學習,大致依難度遞增排序
聯邦平均的典型實作會對資料進行多次本機傳遞 (週期) (除了在多個批次中對資料進行一次傳遞之外)。對於聯邦重建,我們可能希望分別控制重建和重建後訓練的步驟數。將
dataset_split_fn
引數傳遞給訓練和評估運算建構工具,即可控制重建和重建後資料集的步驟數和週期數。作為練習,嘗試執行 3 個本機週期的重建訓練,上限為 50 個步驟,以及 1 個本機週期的重建後訓練,上限為 50 個步驟。提示:您會發現tff.learning.models.ReconstructionModel.build_dataset_split_fn
很有幫助。完成此操作後,請嘗試調整這些超參數和其他相關超參數 (例如學習率和批次大小),以獲得更好的結果。聯邦重建訓練和評估的預設行為是將用戶端的本機資料分成兩半,分別用於重建和重建後處理。在用戶端本機資料非常少的情況下,僅針對訓練程序重複使用資料進行重建和重建後處理可能是合理的 (不適用於評估,這會導致不公平的評估)。嘗試針對訓練程序進行此變更,確保用於評估的
dataset_split_fn
仍然讓重建和重建後資料不相交。提示:tff.learning.models.ReconstructionModel.simple_dataset_split_fn
可能很有用。在上方,我們使用
tff.learning.models.ReconstructionModel.from_keras_model_and_layers
從 Keras 模型產生tff.learning.models.VariableModel
。我們也可以透過 實作模型介面,使用純 TensorFlow 2.0 實作自訂模型。嘗試修改get_matrix_factorization_model
,以建構並傳回擴充tff.learning.models.ReconstructionModel
的類別,並實作其方法。提示:tff.learning.models.ReconstructionModel.from_keras_model_and_layers
的原始碼提供擴充tff.learning.models.ReconstructionModel
類別的範例。另請參考 EMNIST 影像分類教學課程中的自訂模型實作,以瞭解擴充tff.learning.models.VariableModel
的類似練習。在本教學課程中,我們闡述了矩陣分解情境中部分本機聯邦學習的動機,其中將使用者內嵌傳送至伺服器會輕易洩漏使用者偏好。我們也可以在其他設定中應用聯邦重建,作為訓練更個人化模型 (因為模型的一部分完全在本機給每位使用者) 同時減少通訊 (因為本機參數不會傳送至伺服器) 的一種方式。一般來說,使用此處提供的介面,我們可以採用任何通常完全在全球範圍內訓練的聯邦模型,而是將其變數劃分為全球變數和本機變數。聯邦重建論文 中探索的範例是個人下一個字詞預測:在此範例中,每位使用者都有自己的詞彙外字詞的本機字詞內嵌集,讓模型能夠捕捉使用者的俚語並實現個人化,而無需額外通訊。作為練習,嘗試實作 (作為 Keras 模型或自訂 TensorFlow 2.0 模型) 不同的模型,以搭配聯邦重建使用。建議:實作具有個人使用者內嵌的 EMNIST 分類模型,其中個人使用者內嵌會在模型的最後一個密集層之前串連到 CNN 影像特徵。您可以重複使用本教學課程中的許多程式碼 (例如
UserEmbedding
類別) 和 影像分類教學課程。
如果您仍在尋找更多關於部分本機聯邦學習的資訊,請查看 聯邦重建論文 和 開放原始碼實驗程式碼。