사용자 정의 페더레이션 알고리즘, 1부: Federated Core 소개

TensorFlow.org에서 보기 Google Colab에서 실행하기 GitHub에서 소스 보기

이 튜토리얼은 Federated Core(FC)를 사용하여 TensorFlow 페더레이션(TFF)에서 사용자 정의 형태의 페더레이션 알고리즘을 구현하는 방법을 보여주는 2부 시리즈 중 첫 번째입니다. FC는 페더레이션 학습(FL) 레이어를 구현할 때 기반으로 이용된 저수준 인터페이스 세트입니다.

개념적인 내용을 다루는 1부에서는 TFF에 이용되는 핵심 개념과 프로그래밍 추상화에 대해 알아보고 분산된 온도 센서 어레이를 이용한 매우 간단한 예를 통해 실제 사용 방법을 보여드릴 것입니다. 이 시리즈 2부에서는 여기서 소개하는 메커니즘을 이용해 간단한 형태의 페더레이션 훈련 및 평가 알고리즘을 구현합니다. 그 다음 단계로 tff.learning에서 페더레이션 평균화의 구현을 연구해볼 것을 권장합니다.

이 시리즈가 끝날 무렵에는 Federated Core의 적용 범위가 반드시 학습에만 국한되지 않는다는 사실을 알게 될 것입니다. 여기서 제공하는 프로그래밍 추상화는 매우 일반적이며 분산 데이터에 대한 분석과 기타 사용자 정의 유형의 계산을 구현하는 데 사용할 수 있습니다.

이 튜토리얼은 독립적으로 설계되었지만 이미지 분류텍스트 생성에 관한 튜토리얼을 먼저 읽어보면 여기서 설명하는 개념이 더 쉽게 이해될 것이므로 보다 개괄적인 수준에서 부담 없이 TensorFlow 페더레이션 프레임워크와 Federated Learning API(tff.learning)를 배울 수 있을 것입니다.

사용 목적

간단히 말해 Federated Core(FC)는 TensorFlow 코드를 페더레이션 평균화에 사용되는 것과 같은 분산 통신 연산자와 결합하는 프로그램 논리를 간결하게 표현할 수 있게 해주는 개발 환경으로, 시스템에서 일단의 클라이언트 기기에 대해 분산된 합계, 평균 및 기타 형태의 분산된 집계를 계산하고 모델과 매개변수를 이러한 기기에 브로드캐스트하는 경우를 예로 들 수 있습니다.

아마도 tf.contrib.distribute를 알고 계실 겁니다. 그렇다면 자연스럽게 이런 물음이 생깁니다. 이 프레임워크는 어떻게 다를까요? 두 프레임워크 모두 결국은 TensorFlow 계산을 분산시키는 데 목적을 둡니다.

이 개념을 생각하는 한 가지 방법은 tf.contrib.distribute의 명시적인 목표는 사용자가 최소한의 변경으로 기존 모델과 훈련 코드를 사용하여 분산 훈련을 가능하게 하는 것이며 분산 인프라를 활용하여 기존 훈련 코드의 효율을 높이는 방법에 초점이 맞춰져 있는 반면, TFF의 Federated Core가 추구하는 목표는 연구자와 실무자에게 해당 시스템에서 사용할 분산 통신의 특정한 패턴을 명시적으로 제어할 수 있는 수단을 제공하는 것입니다. FC의 중점은 이미 구현된 특정한 분산 훈련 기능이 아니라 분산 데이터 흐름 알고리즘을 표현하기 위한 유연하고 확장 가능한 언어를 제공하는 데 있습니다.

TFF FC API의 주요 대상 사용자 중 하나는 새로운 페더레이션 학습 알고리즘을 실험하고, 시스템 구현의 세부적 부분에 얽매이지 않으면서 분산 시스템에서 데이터 흐름이 조정되는 방식에 영향을 미치는 미묘한 설계상의 선택이 가져오는 결과를 평가하려는 연구자와 실무자입니다. FC API가 목표로 하는 추상화 수준은 연구 간행물에서 페더레이션 학습 알고리즘의 메커니즘을 설명하는 데 사용할 수 있는 의사 코드와 대략 대등합니다(즉, 시스템에 어떤 데이터가 있고 이 데이터가 어떻게 변환되는지를 설명하지만, 개별 지점 간 네트워크 메시지 교환 수준까지 내려가지는 않음).

전체적으로 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
@tff.federated_computation
def hello_world():
  return 'Hello, World!'

hello_world()
b'Hello, World!'

페더레이션 데이터

TFF의 차별화된 기능 중 하나는 페더레이션 데이터에 대한 TensorFlow 기반 계산을 간결하게 표현할 수 있다는 것입니다. 이 튜토리얼에서는 페더레이션 데이터라는 용어를 분산 시스템의 기기 그룹에서 호스팅되는 데이터 항목의 모음을 나타내는 의미로 사용합니다. 예를 들어, 모바일 기기에서 실행되는 애플리케이션은 데이터를 수집하여 중앙 위치에 업로드하지 않고 로컬에 저장할 수 있습니다. 또는 분산된 센서 어레이가 온도 판독값을 수집하여 해당 위치에서 저장할 수 있습니다.

위의 예와 같이 페더레이션 데이터는 TFF에서 일급 객체로 취급됩니다. 즉, 이러한 데이터는 매개변수 및 함수 결과로 나타날 수 있고 유형을 가지고 있습니다. 이 개념을 더 확장하기 위해 페더레이션 데이터세트를 페더레이션 값 또는 페더레이션 유형 값이라고 합니다.

중요하게 이해해야 할 점은 모든 기기에서 수집한 전체 데이터 항목(예: 분산 어레이에 있는 모든 센서의 전체 수집 온도 판독값)을 단일 페더레이션 값으로 모델링한다는 것입니다.

예를 들어, 다음은 클라이언트 기기 그룹에서 호스트하는 페더레이션 부동 소수점 유형을 TFF에서 정의하는 방법입니다. 분산된 센서 어레이에서 생성되는 온도 판독값 모음을 이 페더레이션 유형 값으로 모델링할 수 있습니다.

federated_float_on_clients = tff.FederatedType(tf.float32, tff.CLIENTS)

보다 일반적으로, TFF에서 페더레이션 유형은 개별 기기에 상주하는 데이터 항목인 구성원 구성 요소의 유형 T와 이 유형의 페더레이션 값이 호스트되는 기기의 그룹 G(그리고 세 번째로, 곧이어 언급할 선택적 정보)를 지정하여 정의합니다. 페더레이션 값을 호스트하는 기기의 그룹 G를 값의 배치라고 합니다. 따라서 tff.CLIENTS는 배치의 예입니다.

str(federated_float_on_clients.member)
'float32'
str(federated_float_on_clients.placement)
'CLIENTS'

구성원 구성 요소 T와 배치 G가 있는 페더레이션 유형은 아래와 같이 {T}@G로 간결하게 표현할 수 있습니다.

str(federated_float_on_clients)
'{float32}@CLIENTS'

이 간결한 표기에서 중괄호 {}는 예를 들어, 온도 센서 판독값에서 예상되는 바와 같이 구성원 구성 요소(서로 다른 기기의 데이터 항목)가 다를 수 있으므로 하나의 그룹 차원에서 클라이언트가 페더레이션 값을 구성하는 T 형식 항목의 복수 집합을 공동으로 호스트한다는 사실을 상기시키는 역할을 합니다.

페더레이션 값의 구성원 구성 요소는 일반적으로 프로그래머에게 불투명하다는 점에 유의해야 합니다. 즉, 페더레이션 값은 시스템의 기기 식별자에 의해 입력된 단순한 dict으로 생각해서는 안 됩니다. 이러한 값은 다양한 종류의 분산 통신 프로토콜(예: 집계)을 추상적으로 나타내는 페더레이션 연산자에 의해서만 집합적으로 변환됩니다. 너무 추상적으로 들리더라도 걱정하지 마세요. 곧 이 내용과 관련해 구체적인 예를 들어 설명하겠습니다.

TFF의 페더레이션 유형에는 페더레이션 값의 구성원 구성 요소가 다를 수 있는 유형(위에 보았던 내용)과 모두 동일한 것으로 알려진 유형의 두 가지가 있습니다. 이 유형은 tff.FederatedType 생성자의 세 번째 선택적 all_equal 매개변수에 의해 제어됩니다(기본값은 False).

federated_float_on_clients.all_equal
False

모든 T 유형 구성원 구성 요소가 동일한 것으로 알려진 배치 G가 있는 페더레이션 유형은 T@G로 간결하게 표시할 수 있습니다(즉, {T}@G와 반대로 중괄호를 없애 구성원 구성 요소의 복수 집합이 단일 항목으로 구성된다는 사실을 반영함).

str(tff.FederatedType(tf.float32, tff.CLIENTS, all_equal=True))
'float32@CLIENTS'

실제 시나리오에서 발생할 수 있는 이러한 유형의 페더레이션 값을 보여주는 한 가지 예로 페더레이션 훈련에 참여하는 기기 그룹에 서버에서 브로드캐스트하는 하이퍼 매개변수(예: 학습률, 클리핑 표준 등)를 들 수 있습니다.

또 다른 예로 머신러닝 모델의 매개변수 세트를 서버에서 사전에 훈련한 다음 클라이언트 기기 그룹에 브로드캐스트하고 여기서 이들 매개변수를 각 사용자에 맞게 개인화하는 경우를 들 수 있습니다.

예를 들어, 간단한 1차원 선형 회귀 모델에 대해 한 쌍의 float32 매개변수 ab가 있다고 가정해 보겠습니다. 다음과 같이 TFF에서 사용하기 위해 이러한 모델의 (비 페더레이션) 유형을 구성할 수 있습니다. 인쇄된 유형 문자열의 각중괄호 <>는 명명된 또는 명명되지 않은 튜플에 대한 간결한 TFF 표기입니다.

simple_regression_model_type = (
    tff.NamedTupleType([('a', tf.float32), ('b', tf.float32)]))

str(simple_regression_model_type)
'<a=float32,b=float32>'

위의 dtype만 지정한다는 점에 주목하세요. 스칼라가 아닌 유형도 지원됩니다. 위의 코드에서 tf.float32는 보다 일반적인 tff.TensorType(dtype=tf.float32, shape=[])의 바로 가기 표기입니다.

이 모델이 클라이언트에 브로드캐스트될 때 결과적인 페더레이션 값의 유형은 아래와 같이 표시될 수 있습니다.

str(tff.FederatedType(
    simple_regression_model_type, tff.CLIENTS, all_equal=True))
'<a=float32,b=float32>@CLIENTS'

페더레이션 부동 소수점과 대칭된다는 점에서 이러한 유형을 페더레이션 튜플이라고 합니다. 더 일반적으로, 구성원 구성 요소가 XYZ와 유사한 페더레이션 값을 나타내기 위해 페더레이션 XYZ라는 용어를 자주 사용합니다. 따라서 페더레이션 튜플, 페더레이션 시퀀스, 페더레이션 모델 등과 같은 개념에 대해 이야기할 것입니다.

이제 float32@CLIENTS로 돌아가서, 여러 기기 간에 복제되는 것으로 보이지만 모든 구성원이 동일하기 때문에 실제로는 단일 float32입니다. 일반적으로 모든 동일한 페더레이션 유형, 즉 T@G 형식 중 하나를 비 페더레이션 유형 T와 같은 유형으로 생각할 수 있습니다. 두 경우 모두 실제로는 단일(잠재적으로 복제되기는 하지만) 유형의 항목 T만 있기 때문입니다.

TT@G가 같은 유형인 점을 감안할 때 후자의 유형이 어떤 용도로 사용되는지 궁금할 수 있습니다. 계속 읽어보세요.

배치

설계 개요

앞부분에서 페더레이션 값을 공동으로 호스트할 수 있는 시스템 참가자 그룹인 배치의 개념을 소개했으며 배치의 예시 사양으로 tff.CLIENTS를 사용하는 방법을 보여주었습니다.

배치 개념이 TFF 유형 시스템에 도입해야 할 만큼 기본이 되는 이유를 설명하려면 이 튜토리얼의 시작 부분에서 TFF의 사용 목적에 대해 언급한 내용을 상기해야 합니다.

이 튜토리얼에서는 TFF 코드가 시뮬레이션 환경에서 로컬로 실행되는 모습만 살펴보겠지만 우리의 목표는 안드로이드를 실행하는 모바일 또는 임베디드 기기를 포함해 분산 시스템의 물리적 기기 그룹에서 실행하도록 배포할 수 있는 코드를 작성하는 데 TFF를 사용하는 것입니다. 이러한 각 기기는 시스템에서 수행하는 역할(최종 사용자 기기, 중앙 집중식 코디네이터, 다중 계층 아키텍처의 중간 레이어 등)에 따라 로컬에서 실행할 별도의 명령 세트를 받습니다. 기기의 어떤 하위 집합이 어떤 코드를 실행하는지, 그리고 데이터의 각 부분이 물리적으로 구체화될 수 있는 위치를 추론할 수 있어야 합니다.

이는 예를 들어, 모바일 기기의 애플리케이션 데이터를 다룰 때 특히 중요합니다. 데이터는 비공개이며 민감할 수 있으므로 이 데이터가 기기를 벗어나지 않는다는 것을 정적으로 확인하고 데이터가 처리되는 방식에 대한 사실을 입증할 수 있어야 합니다. 배치 사양은 이를 지원하도록 설계된 메커니즘 중 하나입니다.

TFF는 데이터 중심적 프로그래밍 환경으로 설계되었기 때문에 연산 및 이러한 연산이 실행될 수 있는 위치에 중점을 두는 기존 프레임워크와 달리 TFF는 데이터, 해당 데이터가 구체화되는 위치 및 데이터가 변환되는 방식에 중점을 둡니다. 결과적으로 배치는 데이터에 대한 연산 속성이 아닌 TFF의 데이터 속성으로 모델링됩니다. 실제로 다음 섹션에서 살펴보겠지만 일부 TFF 연산은 여러 위치에 걸쳐 있으며, 말하자면 단일 시스템이나 시스템 그룹에 의해 실행되는 것이 아니라 "네트워크에서" 실행됩니다.

특정 값의 유형을 T@G 또는 {T}@G(T만 있는 경우와 반대로)로 나타내면 데이터 배치 결정이 명확해지고, TFF로 작성된 프로그램의 정적 분석과 함께 민감한 기기 내 데이터에 대한 공식적인 개인 정보 보호를 제공하기 위한 토대 역할을 할 수 있습니다.

그러나 이 시점에서 중요하게 주목해야 할 점은 TFF 사용자가 데이터(배치)를 호스트하는 참여 기기 그룹에 대해 명시적이도록 권장하지만 프로그래머는 개별 참가자의 원시 데이터 또는 ID를 처리하지 않는다는 것입니다.

(참고: 이 튜토리얼의 범위를 훨씬 벗어나지만 위에 한 가지 주목할 예외가 있음을 짚고 넘어가야 합니다. 즉, tff.federated_collect 연산자는 특수한 상황에서만 저수준 기본 요소로 사용된다는 사실입니다. 피할 수 있는 상황에서 이 연산자를 명시적으로 사용하면 향후 가능한 애플리케이션을 제한할 수 있으므로 권장되지 않습니다. 예를 들어, 정적 분석 과정에서 계산에 이러한 낮은 수준의 메커니즘이 사용된다고 판단하면 특정 유형의 데이터에 대한 액세스를 허용하지 않을 수 있습니다.)

TFF 코드 본문 내에는 tff.CLIENTS로 표시되는 그룹을 구성하는 기기를 열거하거나 그룹에 특정 기기가 있는지 조사할 방법이 없도록 설계되어 있습니다. Federated Core API, 기본 아키텍처 추상화 세트 또는 시뮬레이션을 지원하기 위해 제공하는 코어 런타임 인프라 어디에도 기기 또는 클라이언트 ID에 대한 개념이 없습니다. 작성하는 모든 계산 논리는 전체 클라이언트 그룹에 대한 연산으로 표현됩니다.

여기서, 구성원 구성 요소를 단순히 열거할 수 없다는 점에서 페더레이션 유형의 값이 Python dict과 다르다고 앞서 언급했던 말을 상기하기 바랍니다. TFF 프로그램 논리에서 조작하는 값을 개별 참가자가 아닌 배치(그룹)와 연관된 것으로 생각해 보세요.

배치는 또한 TFF에서 일급 객체로 설계되었으며, placement 형식의 매개변수 및 결과로 나타날 수 있습니다(API에서 tff.PlacementType로 표시됨). 향후에 배치를 변환하거나 결합하는 다양한 연산자를 제공할 계획이지만 이 내용은 본 튜토리얼의 범위를 벗어납니다. 지금은 placement를 TFF의 불투명한 기본 내장 유형으로만 생각해도 충분합니다. intbool이 Python에서 불투명한 내장 유형이고1int 유형의 상수 리터럴인 것과 마찬가지로 tff.CLIENTS가 이 유형의 상수 리터럴인 상황과 비슷합니다.

배치 지정

TFF는 두 가지 기본적인 배치 리터럴인 tff.CLIENTStff.SERVER를 제공하여 다수의 클라이언트 기기(휴대전화, 임베디드 기기, 분산 데이터베이스, 센서 등)가 하나의 중앙화된 서버 코디네이터에 의해 조정되는 클라이언트-서버 아키텍처로 자연스럽게 모델링되는 풍부한 실제 시나리오를 쉽게 표현할 수 있도록 합니다. TFF는 또한 사용자 정의 배치, 다중 클라이언트 그룹, 다중 계층 및 기타 더 일반적인 분산 아키텍처를 지원하도록 설계되었지만 이에 대한 논의는 본 튜토리얼의 범위를 벗어납니다.

TFF는 tff.CLIENTS 또는 tff.SERVER가 실제로 나타내는 내용을 규정하지 않습니다.

특히 tff.SERVER는 단일 물리적 기기(단일 그룹의 구성원)일 수 있지만 상태 시스템 복제를 실행하는 내결함성 클러스터의 복제본 그룹일 수도 있습니다. 아키텍처에 대해 특별한 가정을 하지는 않습니다. 하지만 앞 섹션에서 언급한 all_equal 비트를 사용하여 일반적으로 서버에서 단일 데이터 항목만 처리한다는 사실을 표현합니다.

마찬가지로, 일부 애플리케이션에서 tff.CLIENTS는 시스템의 모든 클라이언트를 나타낼 수 있습니다. 페더레이션 학습과 관련해서는 이를 집단이라고 하지만 예를 들어, 페더레이션 평균화의 프로덕션 구현에서는 특정한 훈련 라운드에 참여하도록 선택된 클라이언트의 하위 세트인 코호트를 나타낼 수 있습니다. 추상적으로 정의된 배치는 이러한 배치가 있는 계산이 실행을 위해 배포될 때(또는 이 튜토리얼에서 나타낸 바와 같이 시뮬레이션된 환경에서 Python 함수와 같이 단순히 호출됨) 구체적인 의미를 갖게 됩니다. 로컬 시뮬레이션에서 클라이언트의 그룹은 입력으로 제공된 페더레이션 데이터에 의해 결정됩니다.

페더레이션 계산

페더레이션 계산 선언하기

TFF는 모듈식 개발을 지원하는 강력한 형식의 함수형 프로그래밍 환경으로 설계되었습니다.

TFF의 기본 구성 단위는 페더레이션 값을 입력으로 받아서 페더레이션 값을 출력으로 반환할 수 있는 논리 섹션인 페더레이션 계산입니다. 이전 예로 돌아가 센서 어레이에서 보고하는 온도의 평균을 계산하는 계산을 정의하는 방법은 다음과 같습니다.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def get_average_temperature(sensor_readings):
  return tff.federated_mean(sensor_readings)

위의 코드를 보면 이 시점에서 TensorFlow의 tf.function과 같이 구성 가능한 단위를 정의하기 위한 코디네이터 생성자가 이미 있지 않은지, 있다면 또 다른 요소를 도입하는 이유는 무엇인지, 차이점은 무엇인지 궁금할 수 있을 것입니다.

간단하게 대답하면 tff.federated_computation 래퍼에 의해 생성되는 코드는 TensorFlow도 아니고, Python도 아닙니다. 내부 플랫폼 독립적인 접착(glue) 언어에서 분산 시스템을 특정하게 나타낸 것입니다. 이 시점에서는 이러한 설명이 분명 수수께끼 같이 들리겠지만 페더레이션 계산에 대한 이러한 직관적인 해석을 분산 시스템의 추상적 사양으로 생각해 주기를 바랍니다. 잠시 후에 관련 내용을 설명할 것입니다.

정의에 대해 조금 알아 보겠습니다. TFF 계산은 일반적으로 매개변수가 있거나 없는 함수로 모델링되지만 잘 정의된 형식 서명이 있습니다. 아래와 같이 type_signature 속성을 쿼리하여 계산의 형식 서명을 출력할 수 있습니다.

str(get_average_temperature.type_signature)
'({float32}@CLIENTS -> float32@SERVER)'

형식 서명은 계산이 클라이언트 기기에서 서로 다른 센서 판독값 모음을 받아 서버에서 단일 평균을 반환한다는 사실을 알려줍니다.

더 진행하기 전에 잠시 생각해 보겠습니다. 이 계산의 입력과 출력은 서로 다른 위치에 있습니다(CLIENTSSERVER). 배치에 관한 이전 섹션에서 TFF 연산이 여러 위치에 걸쳐서 네트워크에서 실행될 수 있는 방법에 대해 설명한 내용과 페더레이션 계산이 분산 시스템의 추상적인 사양을 나타낸다고 바로 전해 언급했던 내용을 상기해 보세요. 데이터가 클라이언트 기기에서 소비되고 집계 결과가 서버에 나타나는 단순 분산 시스템을 통해 방금 전에 이러한 한 가지 계산을 정의했습니다.

많은 실제 시나리오에서는 최상위 작업을 나타내는 계산이 입력을 받아들이고 서버에서 출력을 보고하는 경향이 있는데, 그 배경에는 계산이 서버에서 시작되고 종료되는 쿼리에 의해 트리거될 수 있다는 생각이 자리잡고 있습니다.

그러나 FC API는 이러한 가정을 강요하지 않습니다. 내부적으로 사용하는 많은 빌딩 블록(API에서 볼 수 있는 수 많은 tff.federated_... 연산자 포함)에는 고유한 배치를 가진 입력 및 출력이 있습니다. 따라서 일반적으로 페더레이션 계산을 서버에서 실행하거나 서버에 의해 실행되는 것으로 생각하지 않아야 합니다. 서버는 페더레이션 계산에 참여하는 한 유형일 뿐입니다. 이러한 계산의 메커니즘을 고려할 때 중앙화된 단일 코디네이터의 관점보다는 기본적으로 항상 전체 네트워크의 관점을 갖는 것이 좋습니다.

일반적으로 함수형 형식 서명은 각각 입력 및 출력의 유형 TU에 대해 (T -> U)로 간결하게 표현됩니다. 형식 매개변수의 유형(이 경우에는 sensor_readings)은 데코레이터에 대한 인수로 지정됩니다. 결과 유형은 자동으로 결정되므로 지정할 필요가 없습니다.

TFF가 제한적인 형태의 다형성을 제공하기는 하지만 프로그래머는 코드의 속성을 보다 쉽게 이해하고 디버깅하며 공식적으로 검증할 수 있도록 사용하는 데이터 형식에 대해 명시적이어야 합니다. 어떤 경우에는 형식을 명시적으로 지정하는 것이 필수적입니다(예: 다형 계산은 현재 직접적으로 실행할 수 없음).

페더레이션 계산 실행하기

개발 및 디버깅을 지원하기 위해 TFF에서는 아래와 같이 이러한 방식으로 정의된 계산을 Python 함수로 직접 호출할 수 있습니다. 계산에서 all_equal 비트가 False로 설정된 페더레이션 유형의 값을 예상하는 경우 Python에서 일반 list로 이 값을 제공할 수 있으며 all_equal 비트가 True로 설정된 페더레이션의 경우 (단일) 구성원 구성 요소를 직접 제공할 수 있습니다. 결과가 보고되는 방식도 마찬가지입니다.

get_average_temperature([68.5, 70.3, 69.8])
69.53334

시뮬레이션 모드에서 이와 같이 계산을 실행하면, 실제로 입력에서 클라이언트 값을 제공하고 서버 결과를 소비한 여기서의 경우와 같이, 네트워크의 모든 위치에서 입력을 제공하고 출력을 소비할 수 있는 시스템 전체 보기를 가진 외부 관찰자로 역할하게 됩니다.

이제 접착 언어로 코드를 내보내는 tff.federated_computation 데코레이터에 대해 이전에 주지했던 사항으로 돌아가 보겠습니다. TFF 계산의 논리는 Python에서 일반 함수로 표현할 수 있고(위에서와 같이 tff.federated_computation으로 데코레이팅만 해주면 됨), 이 노트북의 다른 Python 함수와 마찬가지로 내부적으로 Python 인수로 직접 호출할 수 있지만, TFF 계산은 실제로는 Python이 아닙니다.

이 말을 다시 표현하자면, Python 인터프리터가 tff.federated_computation으로 데코레이팅된 함수를 발견하면 이 함수의 본문에 있는 문을 한 번(정의 시간에) 추적한 다음, 실행을 위해서건, 다른 계산에 하위 구성 요소로 도입하려는 경우이건 향후 사용을 위해 계산 논리의 직렬화된 표현을 구성한다는 것입니다.

다음과 같이 print 문을 추가하여 이러한 내용을 확인할 수 있습니다.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def get_average_temperature(sensor_readings):

  print ('Getting traced, the argument is "{}".'.format(
      type(sensor_readings).__name__))

  return tff.federated_mean(sensor_readings)
Getting traced, the argument is "ValueImpl".

페더레이션 계산을 정의하는 Python 코드를 즉시 실행이 아닌 컨텍스트에서 TensorFlow 그래프를 빌드하는 Python 코드와 비슷하다고 생각할 수 있습니다(TensorFlow의 즉시 실행 사용에 익숙하지 않다면 연산 그래프를 정의하는 Python 코드를 실제로 실시간으로 실행되지 않고 나중에 실행되는 것으로 생각하면 됨). TensorFlow의 즉시 실행되지 않는 그래프 빌드 코드는 Python이지만 이 코드로 구성된 TensorFlow 그래프는 플랫폼 독립적이며 직렬화 가능합니다.

마찬가지로, TFF 계산은 Python에서 정의되지만 방금 표시된 예제의 tff.federated_mean과 같은 본문의 Python 문은 내부에서 이식 가능하고 플랫폼 독립적인 직렬화 가능한 표현으로 컴파일됩니다.

개발자는 직접 처리할 필요가 없으므로 이러한 표현의 세부 사항에 대해 신경 쓸 필요가 없지만 이러한 부분이 있다는 점과 TFF 계산이 기본적으로 즉시 실행이 아니고 임의의 Python 상태를 포착할 수 없다는 사실을 알고 있어야 합니다. TFF 계산의 본문에 포함된 Python 코드는 tff.federated_computation으로 데코레이팅된 Python 함수의 본문이 직렬화되기 전에 추적되는 정의 시간에 실행됩니다. 호출 시 다시 추적되지 않습니다(함수가 다형인 경우는 제외하며 자세한 내용은 설명서 페이지 참조).

Python이 아닌 전용 내부 표현을 도입하기로 선택한 이유가 궁금할 것입니다. 한 가지 이유는 궁극적으로 TFF 계산은 실제 물리적 환경에 배포할 수 있고 Python을 사용할 수 없는 모바일 또는 임베디드 기기에서 호스트하기 위한 것입니다.

또 다른 이유는 TFF 계산이 개별 참가자의 로컬 동작을 표현하는 Python 프로그램과 달리 분산 시스템의 전역 동작을 표현하기 때문입니다. 클라이언트 기기에서 데이터를 받아들이지만 결과를 서버에 저장하는 특수 연산자 tff.federated_mean을 사용하는 위의 간단한 예에서 이러한 사실을 확인할 수 있습니다.

연산자 tff.federated_mean은 로컬에서 실행되지 않기 때문에 Python에서 일반 연산자로 쉽게 모델링할 수 없습니다. 이전에 언급했듯이 이 연산자는 여러 시스템 참여자의 동작을 조정하는 분산 시스템을 나타냅니다. Python의 일반 (로컬) 연산자와 구분하기 위해 이러한 연산자를 페더레이션 연산자라고 하겠습니다.

TFF 유형 시스템과 TFF 언어에서 지원되는 기본 연산 세트는 Python의 시스템과 크게 다르므로 전용 표현을 사용해야 합니다.

페더레이션 계산 구성하기

위에서 언급했듯이 페더레이션 계산과 그 구성 요소는 분산 시스템의 모델로 생각하는 것이 가장 적합하며 페더레이션 계산을 구성하는 것은 단순한 시스템에서 더 복잡한 분산 시스템을 구성하는 것으로 생각할 수 있습니다. tff.federated_mean 연산자는 형식 서명 ({T}@CLIENTS -> T@SERVER)을 가진 내장 템플릿 페더레이션 계산의 일종으로 생각할 수 있습니다(실제로 작성하는 계산과 마찬가지로 이 연산자도 복잡한 구조를 가지고 있으며 내부적으로는 더 간단한 연산자로 세분함).

페더레이션 계산을 구성할 때도 마찬가지입니다. 계산 get_average_temperaturetff.federated_computation으로 데코레이팅된 다른 Python 함수의 본문에서 호출할 수 있습니다. 이렇게 하면 앞서 tff.federated_mean이 자체 본문에 포함되었던 것과 매우 유사하게 상위 요소의 본문에 포함되게 됩니다.

주의해야 할 중요한 제한 사항은 tff.federated_computation으로 데코레이팅된 Python 함수의 본문이 페더레이션 연산자로만 구성되어야 한다는 것입니다. 즉, TensorFlow 연산을 직접 포함할 수 없습니다. 예를 들어, tf.nest 인터페이스를 직접 사용하여 페더레이션 값 쌍을 추가할 수 없습니다. TensorFlow 코드는 다음 섹션에서 설명하는 tff.tf_computation으로 데코레이팅된 코드 블록으로 제한되어야 합니다. 이 방식으로 래핑된 경우에만 래핑된 TensorFlow 코드를 tff.federated_computation의 본문에서 호출할 수 있습니다.

이렇게 분리하는 이유에는 기술적 측면(비 텐서와 동작하도록 tf.add와 같은 연산자를 속이기 어려움)과 아키텍처 측면이 관련됩니다. 페더레이션 계산의 언어(즉, tff.federated_computation으로 데코레이팅된 Python 함수의 직렬화된 본문에서 구성된 논리)는 플랫폼 독립적인 접착 언어로 사용되도록 설계되었습니다. 이 접착 언어는 현재 TensorFlow 코드가 포함된 섹션에서 분산 시스템을 빌드하는 데 사용됩니다(tff.tf_computation 블록으로 제한됨). 때가 무르익으면 입력 파이프라인을 나타낼 수 있는 관계형 데이터베이스 쿼리와 같이 TensorFlow가 아닌 다른 논리 부분을 모두 동일한 접착 언어(tff.federated_computation 블록)를 사용하여 연결해야 할 것으로 생각합니다.

TensorFlow 논리

TensorFlow 계산 선언하기

TFF는 TensorFlow와 함께 사용하도록 설계되었습니다. 따라서 TFF로 작성하는 코드의 대부분은 일반적인(즉, 로컬에서 실행되는) TensorFlow 코드일 것입니다. 이러한 코드를 TFF와 함께 사용하려면 위에서 언급한 것처럼 tff.tf_computation으로 데코레이트하면 됩니다.

예를 들어, 숫자를 받아 0.5를 더하는 함수를 구현하는 방법은 다음과 같습니다.

@tff.tf_computation(tf.float32)
def add_half(x):
  return tf.add(x, 0.5)

이번에도 여기서 단순히 tf.function과 같은 기존 메커니즘을 사용하지 않고 또 다른 데코레이터인 tff.tf_computation를 정의해야 하는 이유가 궁금할 수 있을 것입니다. 이전 섹션과 달리 여기서는 일반적인 TensorFlow 코드 블록을 다루고 있습니다.

여기에는 몇 가지 이유가 있고 전체적인 내용을 다루는 것은 이 튜토리얼의 범위를 벗어나지만 기본적인 부분은 언급할 가치가 있습니다.

  • TensorFlow 코드를 사용하여 구현된 재사용 가능한 빌딩 블록을 페더레이션 계산의 본문에 포함하려면 정의 시간에 추적 및 직렬화하고 형식 서명을 포함하는 등과 같은 특정 속성을 충족해야 합니다. 이를 위해 일반적으로 일정 형태의 데코레이터가 필요합니다.

일반적으로, TFF의 데코레이터가 즉시 실행 함수와 상호 작용하는 정확한 방식으로 진화할 것으로 예상되므로 가능하면 tf.function과 같은 구성에 TensorFlow의 기본 메커니즘을 사용하는 것이 좋습니다.

이제 위의 예제 코드 조각으로 돌아가서, 방금 정의한 add_half 계산은 다른 TFF 계산과 마찬가지로 TFF에 의해 처리될 수 있습니다. 특히 여기에는 TFF 형식 서명이 있습니다.

str(add_half.type_signature)
'(float32 -> float32)'

이 형식 서명에는 배치가 없습니다. TensorFlow 계산은 페더레이션 유형을 소비하거나 반환할 수 없습니다.

이제 다른 계산에서 add_half를 빌딩 블록으로 사용할 수도 있습니다. 예를 들어, 다음은 tff.federated_map 연산자를 사용하여 클라이언트 기기에서 페더레이션 float의 모든 구성원 구성 요소에 포인트 방식으로 add_half를 적용하는 방법입니다.

@tff.federated_computation(tff.FederatedType(tf.float32, tff.CLIENTS))
def add_half_on_clients(x):
  return tff.federated_map(add_half, x)
str(add_half_on_clients.type_signature)
'({float32}@CLIENTS -> {float32}@CLIENTS)'

TensorFlow 계산 실행하기

tff.tf_computation으로 정의된 계산의 실행은 tff.federated_computation에 대해 설명한 것과 동일한 규칙을 따릅니다. 다음과 같이 Python에서 이러한 계산을 일반 callable로 호출할 수 있습니다.

add_half_on_clients([1.0, 3.0, 2.0])
[<tf.Tensor: shape=(), dtype=float32, numpy=1.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=3.5>,
 <tf.Tensor: shape=(), dtype=float32, numpy=2.5>]

다시 한 번, 이러한 방식으로 add_half_on_clients 계산을 호출하면 분산된 프로세스와 동일한 상황이 벌어진다는 점을 상기하기 바랍니다. 즉, 데이터가 클라이언트에서 소비되고 클라이언트에서 반환됩니다. 실제로 이 계산으로 각 클라이언트가 로컬 작업을 수행합니다. 이 시스템에서 명시적으로 언급된 tff.SERVER는 없습니다(실제로 이러한 처리를 조정하는 데 관련될 수는 있지만). 이러한 방식으로 정의된 계산을 MapReduceMap 단계와 개념적으로 유사하다고 생각할 수 있습니다.

또한, 앞 섹션에서 TFF 계산이 정의 시간에 직렬화된다고 말했던 내용은 tff.tf_computation 코드에도 적용된다는 점에 주목하세요. add_half_on_clients의 Python 본문은 정의 시간에 한 번 추적되고 이후 호출에서는 TFF가 직렬화된 표현을 사용합니다.

tff.federated_computation으로 데코레이팅된 Python 메서드와 tff.tf_computation로 데코레이팅된 메서드 사이의 유일한 차이점은 후자가 TensorFlow 그래프로 직렬화된다는 점입니다(전자는 TensorFlow 코드를 직접 포함할 수 없음).

내부적으로 tff.tf_computation으로 데코레이팅된 각 메서드는 계산의 구조를 포착할 수 있도록 즉시 실행을 일시적으로 비활성화합니다. 즉시 실행은 로컬에서 비활성화되어 있지만 올바르게 직렬화될 수 있는 방식으로 계산 논리를 작성한다면 즉시 실행 TensorFlow, AutoGraph, TensorFlow 2.0 구문 등을 사용할 수 있습니다.

예를 들어, 다음 코드는 실패합니다.

try:

  # Eager mode
  constant_10 = tf.constant(10.)

  @tff.tf_computation(tf.float32)
  def add_ten(x):
    return x + constant_10

except Exception as err:
  print (err)
Attempting to capture an EagerTensor without building a function.

tff.tf_computation 구문이 직렬화 프로세스 중 add_ten 본문에 내부적으로 구성하는 그래프의 밖에서 constant_10가 이미 구성되었기 때문에 위 구문은 실패합니다.

반면에 tff.tf_computation 내부에서 호출될 때 현재 그래프를 수정하는 Python 함수를 호출하는 것은 괜찮습니다.

def get_constant_10():
  return tf.constant(10.)

@tff.tf_computation(tf.float32)
def add_ten(x):
  return x + get_constant_10()

add_ten(5.0)
15.0

TensorFlow의 직렬화 메커니즘이 진화하고 있으며 TFF가 계산을 직렬화하는 방식에 대한 세부 사항도 진화할 것으로 예상합니다.

tf.data.Dataset로 작업하기

앞서 언급했듯이 tff.tf_computation의 고유한 특징은 코드에서 형식 매개변수로 추상적으로 정의된 tf.data.Dataset로 작업할 수 있다는 것입니다. TensorFlow에서 데이터세트로 표현되는 매개변수는 tff.SequenceType 생성자를 사용하여 선언해야 합니다.

예를 들어, 형식 사양 tff.SequenceType(tf.float32)은 TFF에서 부동 요소의 추상 시퀀스를 정의합니다. 시퀀스는 텐서 또는 복잡한 중첩 구조를 포함할 수 있습니다(나중에 관련 예제 설명). T 유형 항목 시퀀스에 대한 간결한 표현은 T*입니다.

float32_sequence = tff.SequenceType(tf.float32)

str(float32_sequence)
'float32*'

온도 센서의 예에서 각 센서가 하나의 온도 판독값이 아니라 여러 개의 온도 판독값을 보유한다고 가정해 보겠습니다. 다음은 TensorFlow에서 tf.data.Dataset.reduce 연산자를 사용하여 단일 로컬 데이터세트의 평균 온도를 계산하는 TFF 계산을 정의하는 방법입니다.

@tff.tf_computation(tff.SequenceType(tf.float32))
def get_local_temperature_average(local_temperatures):
  sum_and_count = (
      local_temperatures.reduce((0.0, 0), lambda x, y: (x[0] + y, x[1] + 1)))
  return sum_and_count[0] / tf.cast(sum_and_count[1], tf.float32)
str(get_local_temperature_average.type_signature)
'(float32* -> float32)'

tff.tf_computation으로 데코레이팅된 메서드 본문에서 TFF 시퀀스 유형의 정식 매개변수는 tf.data.Dataset처럼 동작하는 객체로 간단히 표현됩니다. 즉, 동일한 속성과 메서드를 지원합니다(현재는 해당 유형의 서브 클래스로 구현되지 않지만, TensorFlow의 데이터세트에 대한 지원이 발전함에 따라 달라질 수 있음).

이 내용을 다음과 같이 쉽게 확인할 수 있습니다.

@tff.tf_computation(tff.SequenceType(tf.int32))
def foo(x):
  return x.reduce(np.int32(0), lambda x, y: x + y)

foo([1, 2, 3])
6

일반 tf.data.Dataset와 달리 이러한 데이터세트와 유사한 객체는 자리 표시자입니다. 자리 표시자는 추상적인 시퀀스 유형 매개변수를 나타내기 때문에 구체적인 컨텍스트에서 사용될 때 특정한 데이터에 바인딩되는 요소를 포함하지 않습니다. 추상적으로 정의된 자리 표시자 데이터세트에 대한 지원은 현 시점에서 여전히 다소 제한적이며 TFF 초기에는 특정한 제한이 따를 수 있지만 이 튜토리얼에서는 이에 대해 걱정할 필요가 없습니다(자세한 내용은 문서 페이지 참조).

이 튜토리얼과 같이 시뮬레이션 모드에서 시퀀스를 받아들이는 계산을 로컬에서 실행할 때 아래와 같이 Python 목록으로 시퀀스를 제공할 수 있습니다(그리고 즉시 실행 모드에서 tf.data.Dataset로 시퀀스를 제공할 수 있지만 지금은 간단하게 하겠음).

get_local_temperature_average([68.5, 70.3, 69.8])
69.53333

다른 모든 TFF 유형과 마찬가지로 위에 정의된 것과 같은 시퀀스는 tff.StructType 생성자를 사용하여 중첩된 구조를 정의할 수 있습니다. 예를 들어, 다음은 A, B 쌍의 시퀀스를 받아들이고 그 결과의 합계를 반환하는 계산을 선언하는 방법입니다. 계산 본문에 추적 문을 포함하고 있어 TFF 형식 서명이 데이터세트의 output_typesoutput_shapes로 어떻게 변환되는지 확인할 수 있습니다.

@tff.tf_computation(tff.SequenceType(collections.OrderedDict([('A', tf.int32), ('B', tf.int32)])))
def foo(ds):
  print('element_structure = {}'.format(ds.element_spec))
  return ds.reduce(np.int32(0), lambda total, x: total + x['A'] * x['B'])
element_structure = OrderedDict([('A', TensorSpec(shape=(), dtype=tf.int32, name=None)), ('B', TensorSpec(shape=(), dtype=tf.int32, name=None))])
str(foo.type_signature)
'(<A=int32,B=int32>* -> int32)'
foo([{'A': 2, 'B': 3}, {'A': 4, 'B': 5}])
26

tf.data.Datasets를 형식 매개변수로 사용하기 위한 지원은 이 튜토리얼에 사용된 것과 같은 간단한 시나리오에서 작동하지만 여전히 다소 제한적이고 개선이 이루어지고 있습니다.

종합적으로 살펴보기

이제 페더레이션 설정에서 TensorFlow 계산을 다시 사용해 보겠습니다. 각각 온도 판독값의 로컬 시퀀스를 가진 센서 그룹이 있다고 가정합니다. 센서의 로컬 평균을 다음과 같이 평균화하여 전체 온도 평균을 계산할 수 있습니다.

@tff.federated_computation(
    tff.FederatedType(tff.SequenceType(tf.float32), tff.CLIENTS))
def get_global_temperature_average(sensor_readings):
  return tff.federated_mean(
      tff.federated_map(get_local_temperature_average, sensor_readings))

모든 클라이언트의 모든 로컬 온도 판독값에 대한 단순한 평균이 아니라는 점에 유의하세요. 로컬에서 유지하는 판독값의 수에 따라 각 클라이언트의 기여도 가중치가 필요하기 때문입니다. 이 부분은 독자들이 위의 코드를 업데이트하여 연습해보기 바랍니다. tff.federated_mean 연산자는 가중치를 선택적인 두 번째 인수(페더레이션 부동 소수점일 것으로 예상됨)로 받아들입니다.

또한 get_global_temperature_average에 대한 입력은 이제 페더레이션 부동 소수점 시퀀스가 됩니다. 페더레이션 시퀀스는 일반적으로 페더레이션 학습에서 기기 내 데이터를 나타내는 방식이며 시퀀스 요소는 일반적으로 데이터 배치를 나타냅니다(이 예제는 곧 살펴볼 것임).

str(get_global_temperature_average.type_signature)
'({float32*}@CLIENTS -> float32@SERVER)'

다음은 Python에서 데이터 샘플에 대해 로컬에서 계산을 실행하는 방법입니다. 입력을 제공하는 방식은 이제 listlist로서 이루어진다는 점에 주목하세요. 바깥 목록은 tff.CLIENTS에 의해 표시되는 그룹의 기기를 반복하고 안쪽 목록은 각 기기의 로컬 시퀀스에 있는 요소를 반복합니다.

get_global_temperature_average([[68.0, 70.0], [71.0], [68.0, 72.0, 70.0]])
70.0

이것으로 튜토리얼의 첫 번째 부분을 마칩니다. 2부로 계속 진행하세요.