מדריך למפתחים בנושא התאמה אישית במכשיר

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

ODP מריץ קוד מפתח ב-IsolatedProcess שאין לו גישה ישירה לרשת, לדיסקים מקומיים או לשירותים אחרים שפועלים במכשיר, אבל יש לו גישה למקורות הנתונים הבאים שמאוחסנים באופן מקומי:

  • RemoteData – נתוני מפתח/ערך שלא ניתן לשינוי, שהורדתם מקצוות עורפיים מרוחקים שמנוהלים על ידי מפתחים, אם רלוונטי.
  • LocalData – נתוני מפתח-ערך ניתנים לשינוי ונשמרו באופן מקומי על ידי המפתח, אם רלוונטי.
  • UserData – נתוני משתמשים שסופקו על ידי הפלטפורמה.

סוגי הפלט הבאים נתמכים:

  • פלט עקבי: אפשר להשתמש בפלט הזה בעיבוד מקומי עתידי, כדי ליצור פלט מוצג, להקל על אימון מודלים באמצעות למידת נתונים מאוחדת או להקל על ניתוח סטטיסטי במכשירים שונים באמצעות ניתוח נתונים מאוחד.
    • מפתחים יכולים לכתוב בקשות וגם את תוצאות העיבוד שלהן בטבלה המקומית REQUESTS.
    • מפתחים יכולים לכתוב טבלה EVENTS עם נתונים נוספים שמשויכים לבקשה קודמת.
  • הפלט המוצג:
    • מפתחים יכולים להחזיר HTML שעבר רינדור על ידי ODP ב-WebView בתוך SurfaceView. התוכן שעבר עיבוד שם לא יהיה גלוי לאפליקציה שמפעילה אותו.
    • מפתחים יכולים להטמיע כתובות URL של אירועים שסופקו על ידי ODP בתוך הפלט של ה-HTML כדי להפעיל את הרישום ביומן ואת העיבוד של האינטראקציות של המשתמשים עם ה-HTML שעבר עיבוד. ODP מיירט בקשות לכתובות ה-URL האלה ומפעיל קוד ליצירת נתונים שנכתבים בטבלה EVENTS.

אפליקציות לקוח ו-SDK יכולים להפעיל את ODP כדי להציג תוכן HTML ב-SurfaceView באמצעות ממשקי ה-API של ODP. תוכן שמעובד ב-SurfaceView לא גלוי לאפליקציית השיחות. אפליקציית הלקוח או ה-SDK יכולים להיות ישות שונה מזו שפותחת עם ODP.

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

אפליקציות לקוח משתמשות ב-methods במחלקה OnDevicePersonalizationManager כדי לקיים אינטראקציה עם הקוד של המפתח שרץ ב-IsolatedProcess. קוד של המפתח שרץ ב-IsolatedProcess מרחיבים את המחלקה IsolatedService ומטמיע את הממשק IsolatedWorker. ה-IsolatedService צריך ליצור מופע של IsolatedWorker לכל בקשה.

בתרשים הבא מוצג הקשר בין השיטות ב-OnDevicePersonalizationManager וב-IsolatedWorker.

תרשים של הקשר בין OnDevicePersonalizationManager לבין IsolatedWorker.

אפליקציית לקוח קוראת ל-ODP באמצעות השיטה execute עם IsolatedService בעל שם. שירות ODP מעביר את הקריאה ל-method‏ onExecute של ה-IsolatedWorker. השדה IsolatedWorker מחזיר רשומות שיישארו ואת התוכן להצגה. שירות ה-ODP כותב את הפלט הקבוע בטבלה REQUESTS או EVENTS, ומחזיר לאפליקציית הלקוח הפניה אטומה לפלט המוצג. אפליקציית הלקוח יכולה להשתמש בהפניה האטומה הזו בקריאה עתידית ל-requestSurfacePackage כדי להציג את התוכן המוצג בממשק המשתמש שלה.

פלט מתמיד

שירות ODP שומר רשומה בטבלה REQUESTS אחרי שהחזרות של הטמעת onExecute על ידי המפתח מגיעות. כל רשומה בטבלה REQUESTS מכילה נתונים נפוצים שמופקים לפי בקשה, שנוצרו על ידי שירות ה-ODP, לצד רשימה של Rows שהוחזרו. כל Row מכיל רשימה של זוגות (key, value). כל ערך הוא סקלר, מחרוזת או blob. ניתן לקבל דיווח על הערכים המספריים אחרי צבירת נתונים, והדיווח על נתוני המחרוזת או ה-blob יכול להתבצע אחרי החלה של פרטיות דיפרנציאלית מקומית או מרכזית. המפתחים יכולים גם לכתוב בטבלה EVENTS אירועים עתידיים של אינטראקציות של משתמשים – כל רשומה בטבלה EVENTS משויכת לשורה בטבלה REQUESTS. שירות ה-ODP מתעד באופן שקוף חותמת זמן ושם החבילה של אפליקציית השיחות ושל ה-APK של מפתח ה-ODP עם כל רשומה.

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

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

הגדרות של מניפסט החבילה

כדי להשתמש ב-ODP צריך:

  1. תג <property> ב-AndroidManifest.xml שמפנה למשאב XML בחבילה שמכיל את פרטי ההגדרה של ODP.
  2. תג <service> ב-AndroidManifest.xml שמזהה את הכיתה שמרחיבה את IsolatedService, כפי שמוצג בדוגמה הבאה. לשירות בתג <service> צריך להגדיר את המאפיינים exported ו-isolatedProcess כ-true.
  3. תג <service> במשאב ה-XML שצוין בשלב 1, שמזהה את סוג השירות משלב 2. התג <service> חייב לכלול גם הגדרות נוספות שספציפיות ל-ODP בתוך התג עצמו, כפי שמוצג בדוגמה השנייה.

AndroidManifest.xml

<!-- Contents of AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.odpsample" >
    <application android:label="OdpSample">
        <!-- XML resource that contains other ODP settings. -->
        <property android:name="android.ondevicepersonalization.ON_DEVICE_PERSONALIZATION_CONFIG"
                  android:resource="@xml/OdpSettings"></property>
        <!-- The service that ODP binds to. -->
        <service android:name="com.example.odpsample.SampleService"
                android:exported="true" android:isolatedProcess="true" />
    </application>
</manifest>

מניפסט ספציפי ל-ODP במשאב XML

קובץ משאב ה-XML שצוין בתג <property> צריך גם להצהיר על סוג השירות בתג <service>, ולציין את נקודת הקצה של כתובת ה-URL שממנה ODP יוריד תוכן כדי לאכלס את הטבלה RemoteData, כמו בדוגמה הבאה. אם אתם משתמשים בתכונות של מחשוב מאוחד, עליכם לציין גם את נקודת הקצה של כתובת ה-URL של שרת המחשוב המאוחד שאליה יתחבר לקוח המחשוב המאוחד.

<!-- Contents of res/xml/OdpSettings.xml -->
<on-device-personalization>
   <!-- Name of the service subclass -->
   <service name="com.example.odpsample.SampleService">
     <!-- If this tag is present, ODP will periodically poll this URL and
          download content to populate REMOTE_DATA. Developers that do not need to
          download content from their servers can skip this tag. -->
     <download-settings url="https://example.com/get" />
     <!-- If you want to use federated compute feature to train a model, you
          need to specify this tag. -->
     <federated-compute-settings url="https://fcpserver.example.com/" />
   </service>
</on-device-personalization>

הפעלת מצב פיתוח

מפעילים את מצב הפיתוח לפי ההוראות בקטע הפעלת אפשרויות למפתחים במסמכי התיעוד של Android Studio.

הגדרות החלפה וסימון

ב-ODP יש קבוצה של מתגים ודגלים שמשמשים לשליטה בפונקציות מסוימות:

  • _global_killswitch: המתג הגלובלי לכל תכונות ODP. מגדירים אותו כ-false כדי להשתמש ב-ODP.
  • _federated_compute_kill_switch: _המתג ששולט בכל הפונקציות של ODP בנוגע לאימון (למידה משותפת). צריך להגדיר אותו כ-false כדי להשתמש באימון.
  • _caller_app_allowlist: אמצעי בקרה למי מותר לקרוא ODP, ניתן להוסיף כאן אפליקציות (שם pkg, אישור [אופציונלי]) או להגדיר אותו כ-* כדי לאפשר הכול
  • _isolated_service_allowlist: קובע אילו שירותים יוכלו לפעול בתהליך 'שירות מבודד'.

אתם יכולים להריץ את הפקודות הבאות כדי להגדיר את כל המתגים והדגלים לשימוש ב-ODP ללא הגבלות:

# Set flags and killswitches
adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put on_device_personalization global_kill_switch false
adb shell device_config put on_device_personalization federated_compute_kill_switch false
adb shell device_config put on_device_personalization caller_app_allow_list \"*\"
adb shell device_config put on_device_personalization isolated_service_allow_list \"*\"

ממשקי API בצד המכשיר

כדאי לעיין במאמרי העזרה של Android API ל-ODP.

אינטראקציות עם IsolatedService

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

המפתחים צריכים להטמיע את השיטות מהממשק IsolatedWorker כדי לטפל בבקשות של אפליקציות לקוח, בהשלמות של הורדות ובאירועים שמופעל על ידי ה-HTML שעבר עיבוד. לכל השיטות האלה יש הטמעות ברירת מחדל ללא פעולה (no-op), כך שמפתחים יכולים לדלג על הטמעת השיטות שלא מעניינות אותם.

הכיתה OnDevicePersonalizationManager מספקת ממשק API לאפליקציות ולערכות SDK כדי ליצור אינטראקציה עם IsolatedService שהמפתח הטמיע ופועל בתהליך מבודד. ריכזנו כאן כמה תרחישים לדוגמה:

יצירת תוכן HTML להצגה ב-SurfaceView

כדי ליצור תוכן להצגה, באמצעות OnDevicePersonalizationManager#execute, אפליקציית הקריאה יכולה להשתמש באובייקט SurfacePackageToken שהוחזר בקריאה requestSurfacePackage עוקבת כדי לבקש שהתוצאה תומרן ב-SurfaceView.

אם הפעולה מסתיימת בהצלחה, המקבל נקרא עם SurfacePackage עבור תצוגה ששירות ODP עיבד. אפליקציות לקוח צריכות להוסיף את SurfacePackage ל-SurfaceView בהיררכיית התצוגה שלהן.

כשאפליקציה מבצעת קריאה ל-requestSurfacePackage עם SurfacePackageToken שהוחזר על ידי קריאה קודמת ל-OnDevicePersonalizationManager#execute, שירותי ODP קוראים ל-IsolatedWorker#onRender כדי לאחזר את קטע ה-HTML שצריך להציג בתוך מסגרת מוקפת. למפתח אין גישה אל LocalData או UserData במהלך השלב הזה. כך המפתח לא יוכל להטמיע UserData שעשוי להיות רגיש בכתובות URL לאחזור נכסים ב-HTML שנוצר. מפתחים יכולים להשתמש ב-IsolatedService#getEventUrlProvider כדי ליצור כתובות URL למעקב שייכללו ב-HTML שנוצר. כשה-HTML ימומש, שירות ODP יפריע לבקשות לכתובות ה-URL האלה ויבצע קריאה ל-IsolatedWorker#onEvent. אפשר להפעיל את getRemoteData() כשמטמיעים את onRender().

מעקב אחר אירועים בתוכן HTML

הכיתה EventUrlProvider מספקת ממשקי API ליצירת כתובות URL למעקב אחר אירועים, שמפתחים יכולים לכלול בפלט ה-HTML שלהם. במהלך עיבוד ה-HTML, ה-ODP יפעיל את IsolatedWorker#onEvent עם המטען הייעודי (payload) של כתובת ה-URL של האירוע.

שירות ה-ODP מיירט בקשות לכתובות URL של אירועים שנוצרו על ידי ODP בתוך ה-HTML שעבר עיבוד, קורא ל-IsolatedWorker#onEvent ומתעד את EventLogRecord שהוחזרו בטבלה EVENTS.

כתיבת תוצאות קבועות

באמצעות OnDevicePersonalizationManager#execute, השירות יכול לכתוב נתונים באחסון מתמיד (טבלאות REQUESTS ו-EVENTS). אלה הרשומות שאפשר לכתוב בטבלאות האלה:

  • RequestLogRecord להוספה לטבלה REQUESTS.
  • רשימה של EventLogRecord אובייקטים להוספה לטבלה EVENTS, שכל אחד מהם מכיל מצביע ל-RequestLogRecord שנכתב בעבר .

למידה משותפת (Federated) יכולה לקבל תוצאות קבועות מאחסון במכשיר לצורך אימון מודלים.

ניהול משימות אימון במכשיר

שירות ODP מבצע קריאה ל-IsolatedWorker#onTrainingExample כשמתחילה משימה מאוחדת של אימון מחשוב, והוא רוצה לקבל דוגמאות לאימון שסופקו על ידי מפתחים שמשתמשים ב-ODP. אפשר להפעיל את getRemoteData(), getLocalData(), getUserData() ו-getLogReader() כשמטמיעים את onTrainingExample().

כדי לתזמן או לבטל משימות מחשוב מאוחדות, אפשר להשתמש בכיתה FederatedComputeScheduler שמספקת ממשקי API לכל ODP IsolatedService. אפשר לזהות כל משימה של מחשוב מאוחד לפי שם האוכלוסייה שלה.

לפני שמתזמנים משימה חדשה של מחשוב מאוחד:

  • כבר אמורה להיות משימה בשם האוכלוסייה הזה בשרת המחשוב המאוחד והמרוחק.
  • צריך לציין את נקודת הקצה של כתובת URL של שרת מחשוב מאוחד בהגדרות של מניפסט החבילה באמצעות התג federated-compute-settings.

אינטראקציות עם פלט קבוע

בקטע הבא מוסבר איך לבצע אינטראקציה עם פלט עקבי ב-ODP.

קריאת טבלאות מקומיות

המחלקה LogReader מספקת ממשקי API לקריאת הטבלאות REQUESTS ו-EVENTS. הטבלאות האלה מכילות נתונים שנכתבו על ידי IsolatedService במהלך קריאות ל-onExecute() או ל-onEvent(). אפשר להשתמש בנתונים בטבלאות האלה כדי לאמן מודלים באמצעות למידה משותפת (Federated), או כדי לבצע ניתוח סטטיסטי חוצה-מכשירים באמצעות Federated Analytics.

אינטראקציות עם תוכן שהורד

בקטע הבא מוסבר איך ליצור אינטראקציה עם תוכן שהורדתם ב-ODP.

הורדת תוכן מהשרתים

שירות ODP מוריד מדי פעם תוכן מכתובת ה-URL שהוצהרה במניפסט החבילה של IsolatedService, ומבצע קריאה אל onDownloadCompleted אחרי שההורדה מסתיימת. ההורדה היא קובץ JSON שמכיל צמדי מפתח/ערך.

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

הפורמט של בקשת ההורדה

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

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

פורמט הקובץ להורדה

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

השדה content הוא רשימה של צמדים (מפתח, נתונים, קידוד). הערך key צריך להיות מחרוזת בתקן UTF-8. השדה encoding הוא פרמטר אופציונלי שמציין את אופן הקידוד של השדה data. ניתן להגדיר אותו ל-"utf8" או ל-"base64", וברירת המחדל היא שהשדה 'utf8' הוא 'utf8'. השדה key ממיר לאובייקט String והשדה data ממיר למערך בייטים לפני הקריאה ל-onDownloadCompleted().

{
  // syncToken must be a UTC timestamp clamped to an hour boundary, and must be
  // greater than the syncToken of the previously completed download.
  "syncToken": <timeStampInSecRoundedToUtcHour>,
  "contents": [
    // List of { key, data } pairs.
    { "key": "key1",
      "data": "data1"
    },
    { "key": "key2",
      "data": "data2",
      "encoding": "base64"
    },
    // ...
  ]
}

ממשקי API בצד השרת

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

ממשקי API של שרת מחשוב מאוחד

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

תרשים של הטופולוגיה של שרת-לקוח במחשוב המאוחד.

כשיוצרים משימה חדשה ל-Task Builder, מפתחי ODP צריכים לספק שתי קבוצות של קבצים:

  1. מודל tff.learning.models.FunctionalModel שנשמר באמצעות קריאה ל-API‏ tff.learning.models.save_functional_model. דוגמה אחת זמינה במאגר שלנו ב-GitHub.
  2. קובץ fcp_server_config.json שכולל מדיניות, הגדרת למידה משותפת והגדרה של פרטיות דיפרנציאלית. דוגמה לקובץ fcp_server_config.json:
{
  # Task execution mode.
  mode: TRAINING_AND_EVAL
  # Identifies the set of client devices that participate.
  population_name: "mnist_cnn_task"
  policies {
    # Policy for sampling on-device examples. It is checked every
    # time a device is attempting to start a new training.
    min_separation_policy {
      # The minimum separation required between two successful
      # consective task executions. If a client successfully contributes
      # to a task at index `x`, the earliest they can contribute again
      # is at index `(x + minimum_separation)`. This is required by
      # DP.
      minimum_separation: 1
    }
    data_availability_policy {
      # The minimum number of examples on a device to be considered
      # eligible for training.
      min_example_count: 1
    }
    # Policy for releasing training results to developers adopting ODP.
    model_release_policy {
      # The maximum number of training rounds.
      num_max_training_rounds: 512
    }
  }

  # Federated learning setups. They are applied inside Task Builder.
  federated_learning {
    # Use federated averaging to build federated learning process.
    # Options you can choose:
      # * FED_AVG: Federated Averaging algorithm
      #            (https://arxiv.org/abs/2003.00295)
      # * FED_SGD: Federated SGD algorithm
      #            (https://arxiv.org/abs/1602.05629)
    type: FED_AVG
    learning_process {
      # Optimizer used at client side training. Options you can choose:
      # * ADAM
      # * SGD
      client_optimizer: SGD
      # Learning rate used at client side training.
      client_learning_rate: 0.02
      # Optimizer used at server side training. Options you can choose:
      # * ADAM
      # * SGD
      server_optimizer: SGD
      # Learning rate used at server side training.
      server_learning_rate: 1.0
      runtime_config {
        # Number of participating devices for each round of training.
      report_goal: 2
      }
      metrics {
        name: "sparse_categorical_accuracy"
      }
    }
    evaluation {
      # A checkpoint selector controls how checkpoints are chosen for
      # evaluation. One evaluation task typically runs per training
      # task, and on each round of execution, the eval task
      # randomly picks one checkpoint from the past 24 hours that has
      # been selected for evaluation by these rules.
      # Every_k_round and every_k_hour are definitions of quantization
      # buckets which each checkpoint is placed in for selection.
      checkpoint_selector: "every_1_round"
      # The percentage of a populate that should delicate to this
      # evaluation task.
      evaluation_traffic: 0.2
      # Number of participating devices for each round of evaluation.
      report_goal: 2
    }
  }

  # Differential Privacy setups. They are enforced inside the Task
  # Builder.
  differential_privacy {
    # * fixed_gaussian: DP-SGD with fixed clipping norm described in
    #                   "Learning Differentially Private Recurrent
    #                   Language Models"
    #                   (https://arxiv.org/abs/1710.06963).
    type: FIXED_GAUSSIAN
    #   The value of the clipping norm.
    clip_norm: 0.1
    # Noise multiplier for the Gaussian noise.
    noise_multiplier: 0.1
  }
}

דוגמאות נוספות זמינות במאגר שלנו ב-GitHub.

אחרי שמכינים את שני מקורות הקלט האלה, מפעילים את Task Builder כדי ליצור ארטיפקטים וליצור משימות חדשות. הוראות מפורטות יותר זמינות במאגר שלנו ב-GitHub.