Personalizeのドメイン最適化レコメンデーションを使ってみた
データアナリティクス事業本部機械学習チームの中村です。
今回は、Amazon Personalizeで使用可能となったドメイン特化レコメンデーションをご紹介し、実際に使ってみます。
冒頭まとめ
-
PersonalizeでVIDEO ON DEMANDとEコマースのドメインに特化されたレコメンデーションができるようになった。
-
Domain dataset groupがドメイン特化、Custom dataset groupが従来のレコメンデーションという扱い。
-
ドメイン特化レコメンデーションのメリットは以下。
- 従来よりも簡単な手数でリアルタイム推論まで構築できる。
- チューニングなどが必要なしに構築が可能。
-
その反面、以下のような特性があるので要注意が必要。
- 性能を見ながらパラメータの手動チューニング等ができない。
- 現段階では、バッチレコメンドに対応していない。
- 構築フローが異なることにより、料金体系が異なる。
- ユーザーIDを見ながら購入済みの商品はレコメンドされない等、暗黙的なフィルター処理が存在するレシピがある。
リリースについて
Amazon Personalizeは、開発者がパーソナライズされた体験をユーザーに提供することを容易にする、フルマネージドな機械学習サービスであり、Amazonがパーソナライゼーションシステム構築で培った知識と経験が反映された技術を利用可能です。
以下のアップデートにより、あるドメインに最適化されたDomain dataset group
が使用可能となりました。
またこれまでのレコメンド処理はCustom dataset group
と、より広範な用途に使えるカスタムなDataset groupという位置づけとなりました。
本記事では、新たにリリースされたDomain dataset group
と今までのCustom dataset group
の違いを整理し、
実際に、Domain dataset group
のワークフローで、ドメインに最適化されたレコメンデーション環境を構築してみます。
構築は、PythonのSDK(boto3)を用いて実施しますが、マネジメントコンソールからでもほぼ同等のことが実施可能です。
各Dataset groupの概要
今回のリリースにより、2種類のDataset groupができました。
-
Domain dataset group
- あるドメインに最適化されたレコメンドを使用するためのDataset groupです。
- 現時点では、VIDEO_ON_DEMANDとECOMMERCEという2パターンのドメイン向けに準備されています。
- こちらでは、口述の
Custom dataset group
側と同じレコメンド処理(Solutionと呼ばれる)も構築可能です。
-
Custom dataset group
- より広範な用途に使えるカスタムなDataset groupです。
- こちらは、後で
Domain dataset group
のレコメンドを追加させることはできません。- これは、データに必須の項目(列)が、
Domain dataset group
の方がより多いためと考えられます。
- これは、データに必須の項目(列)が、
以降簡単のため、それぞれDomain
・Custom
と略して表記します。
ワークフローの違い
ワークフローが大きく変わっています。Domain
はより少ないステップで、実際のレコメンデーション取得まで環境を構築できます。
以下に図でワークフローを比較します。(各ステップはそれぞれSDKのAPI単位で分けて表現しています)
Custom
の場合- recommendationを取得するまでに、create_solution -> create_solution_version -> create_campaign と段階を踏む必要がありました。
- ここでrecommendation取得はリアルタイム取得のことを指しています。
Domain
の場合- create_recommenderだけで、recommendationが取得できます。
- これによりcampaignがすでに作られている状態と同等となるため、recommender作成時点で料金が発生します。
- import jobを作成する側はフローに差がありません。
- create_recommenderだけで、recommendationが取得できます。
スキーマの違い
スキーマは、学習するデータなどのフォーマットを定義したものです。S3に配置するデータはこのスキーマに沿ったカラムを有している必要があります。
Custom
の場合- https://docs.aws.amazon.com/personalize/latest/dg/custom-datasets-and-schemas.html
Domain
と比較して、必須な項目(Required fields)は最小限となっています。
Dataset type | Required fields | Reserved keywords |
---|---|---|
Interactions | USER_ID (string) ITEM_ID (string) TIMESTAMP (long) |
EVENT_TYPE (string) EVENT_VALUE (float, null) IMPRESSION (string) RECOMMENDATION_ID (string, null) |
Users | USER_ID (string) 1 metadata field |
|
Items | ITEM_ID 1 metadata field |
CREATION_TIMESTAMP (long) |
Domain
の場合(VIDEO_ON_DEMAND)- https://docs.aws.amazon.com/personalize/latest/dg/VIDEO-ON-DEMAND-datasets-and-schemas.html
Custom
と比較して、必須な項目(Reauired fields)がいくつか増えています。- 今回は、Interactionsのみを使いますので、EVENT_TYPEのみが追加で必須な項目となります。
- EVENT_TYPEは値としても、
Watch
は必須、Click
はレシピによっては記録が推奨されています。 - また予約語(Reserved Keywords)も多く増えているため、注意が必要です。
Dataset type | Required fields | Reserved keywords |
---|---|---|
Interactions | USER_ID (string) ITEM_ID (string) TIMESTAMP (long) EVENT_TYPE (string and depending on use case, Watch and Click event types) |
EVENT_VALUE (float, null) IMPRESSION (string) RECOMMENDATION_ID (string, null) |
Users | USER_ID (string) 1 metadata field |
|
Items | ITEM_ID (string) CREATION_TIMESTAMP (long) GENRE_L1 (categorical string) |
PRICE (float, null) DURATION (float, null) GENRE_L2 (categorical string) GENRE_L3 (categorical string) AVERAGE_RATING (float, null) PRODUCT_DESCRIPTION (textual) CONTENT_OWNER (categorical string) CONTENT_CLASSIFICATION (categorical string) |
Domain
の場合(ECOMMERCE)- https://docs.aws.amazon.com/personalize/latest/dg/ECOMMERCE-datasets-and-schemas.html
- VIDEO_ON_DEMANDと同様、
Custom
と比較して、必須な項目(Reauired fields)がいくつか増えています。 - EVENT_TYPEの値は、レシピによって必須なものが異なり、
View
もしくはPurchase
いずれかが必須、レシピによってはもう一方も推奨のものがあります。 - 今回はVIDEO_ON_DEMAND側でトライアルしますので、ECOMMERCE側の詳細な説明は省略いたします。
Dataset type | Required fields | Reserved keywords |
---|---|---|
Interactions | USER_ID (string) ITEM_ID (string) TIMESTAMP (long) EVENT_TYPE (string and depending on use case, Purchase and View event types) |
EVENT_VALUE (float, null) IMPRESSION (string) RECOMMENDATION_ID (string, null) |
Users | USER_ID (string) 1 metadata field |
|
Items | ITEM_ID (string) PRICE (float) CATEGORY_L1 (categorical string) |
CATEGORY_L2 (categorical string) CATEGORY_L3 (categorical string) PRODUCT_DESCRIPTION (textual) CREATION_TIMESTAMP (long) AGE_GROUP (categorical string) ADULT (categorical string) GENDER (categorical string) |
レシピの違い
Recipe | Recipe Types | Required datasets | 説明 |
---|---|---|---|
Popularity-Count | USER_PERSONALIZATION | Interactions | Interactionsデータから最も人気のアイテムをレコメンドします。 このレシピは、すべてのユーザーに対して同じアイテムをレコメンドするタイプです。 そのため、ベースラインのレシピとして使用できます。 |
HRNN | USER_PERSONALIZATION | Interactions | HRNN(Hierachical RNN)モデルを使ったレシピです。 このレシピはlegacyであり、改善・統合されたUser-Personalizationレシピの使用が推奨です。 |
HRNN-Metadata | USER_PERSONALIZATION | Interactions at least 1 metadata |
HRNNレシピに、metadataから推測される特徴を考慮したものです。 metadataは、Interactions, Users, Itemsそれぞれに含まれるデータで、いずれか一つのmetadataが必要となります。 HRNNと同様にlegacyレシピです。 |
HRNN-Coldstart | USER_PERSONALIZATION | Interactions Items |
コールドスタート問題と一般的に呼ばれるものに対応した、新しいItemやInteractionに強いモデルです。 期間が短く、Interactionの数が一定数以下のItemをCold Itemとして扱って処理します。 HRNNと同様にlegacyレシピです。 |
User-Personalization | USER_PERSONALIZATION | Interactions | 3つのlegacyレシピを統合したレシピとなります。 それ以外にモデルの自動更新や、IMPRESSIONデータ(ユーザーにレコメンド表示したデータ)の情報を使用することがでます。 |
Personalized-Ranking | PERSONALIZED_RANKING | Interactions | ITEM_IDのリストをリクエストすると、ユーザーに応じたランキングに並び変えた結果を取得するレシピです。 存在しないITEM_IDが与えられた場合でもエラーとはなりませんが、最後尾に配置されます。 |
SIMS | RELATED_ITEMS | Interactions | Interactionsデータのみを用いて、類似アイテムを取得するレシピです。 Itemの人気度合いとItem間の相関性のバランスをとって計算します。 |
Similar-Items | RELATED_ITEMS | Interactions Items |
InteractionsデータとItemsデータのmetadataおよび非構造化データ(数値やテキストなど)を使って、類似アイテムを取得するレシピです。 Itemsがない場合は、SIMSを使う必要があります。 |
Item-Affinity | USER_SEGMENTATION | Interactions | Interactionsデータからユーザーをセグメンテーションします。 バッチジョブにのみ対応しているため、詳細は省略いたします。 |
Item-Attribute-Affinity | USER_SEGMENTATION | Interactions Items |
InteractionsデータとItemの属性からユーザーをセグメンテーションします。 バッチジョブにのみ対応しているため、詳細は省略いたします。 |
Domain
の場合(VIDEO_ON_DEMAND)- https://docs.aws.amazon.com/personalize/latest/dg/VIDEO_ON_DEMAND-use-cases.html
- userIdに基づいて、視聴済みのVideoはレコメンドされないようなフィルタリングも自動的に実施されます。
Recipe | Required datasets | 説明 |
---|---|---|
Most popular | Interactions(1000件以上のWatchを含む) | 最も人気のVideoをレコメンドするレシピです。 Custom側のPopularity-Countレシピと似た扱いになると考えられます。 |
Because you watched X | Interactions(1000件以上のWatchを含む) | 指定したVideoを元に他のユーザーも見ているVideoをレコメンドしてもらうレシピです。 Interactionにより重点を置いてレコメンドするレシピと考えられます。 userIdに基づいて、既にWatch済みのアイテムは自動的にフィルタリングされます。 |
More like X | Interactions(1000件以上のWatchを含む。Clickも推奨) Items |
指定したVideoと類似したVideoをレコメンドしてもらうレシピです。 InteractionsやItemsのmetadataや非構造化データにより重点をおいてレコメンドするレシピと考えられます。 userIdに基づいて、既にWatch済みのアイテムは自動的にフィルタリングされます。 |
Top picks for you | Interactions(1000件以上のWatchを含む。Clickも推奨) Items |
ユーザー個人向けにパーソナライズされたレコメンドを取得するレシピです。 userIdに基づいて、既にWatch済みのアイテムは自動的にフィルタリングされます。 |
Domain
の場合(ECOMMERCE)- https://docs.aws.amazon.com/personalize/latest/dg/ECOMMERCE-use-cases.html
- userIdに応じて購入済みのものがレコメンドされないレシピもあります。
- 用途によっては購入済みの商品のレコメンドが必要とされるケースもあるため、その場合は用途に合ったレシピを選ぶことが必要です。
Recipe | Required datasets | 説明 |
---|---|---|
Most viewed | Interactions(1000件以上のViewを含む) | 最もViewされている商品をレコメンドするレシピです。 Custom側のPopularity-Countレシピと似た扱いになると考えられます。 |
Best sellers | Interactions(1000件以上のPurchaseを含む) | 最もPurchaseされている商品をレコメンドするレシピです。 こちらもCustom側のPopularity-Countレシピと似た扱いになると考えられます。 |
Frequently bought together | Interactions(1000件以上のPurchaseを含む) | 指定した商品と一緒に購入されることが多い商品をレコメンドしてもらうレシピとなります。 そのため、InteractionsのPurchaseデータを元にしたレシピとなっていると考えられます。 |
Customers who viewed X also viewed | Interactions(1000件以上のViewを含む。Purchaseも推奨) | 指定した商品に基づいて、その商品と同時によく見られている商品をレコメンドするレシピです。 userIdに基づいて、既にPurchase済みのアイテムは自動的にフィルタリングされます。 |
Recommended for you | Interactions(1000件以上のViewを含む。Purchaseも推奨) | ユーザー個人向けにパーソナライズされたレコメンドを取得するレシピです。 userIdに基づいて、既にPurchase済みのアイテムは自動的にフィルタリングされます。 |
- 上記に紐づく、recipeのARNは以下となります。
- arn:aws:personalize:::recipe/aws-popularity-count
- arn:aws:personalize:::recipe/aws-hrnn
- arn:aws:personalize:::recipe/aws-hrnn-metadata
- arn:aws:personalize:::recipe/aws-hrnn-coldstart
- arn:aws:personalize:::recipe/aws-user-personalization
- arn:aws:personalize:::recipe/aws-personalized-ranking
- arn:aws:personalize:::recipe/aws-sims
- arn:aws:personalize:::recipe/aws-similar-items
- arn:aws:personalize:::recipe/aws-item-affinity
- arn:aws:personalize:::recipe/aws-item-attribute-affinity
- arn:aws:personalize:::recipe/aws-vod-most-popular
- arn:aws:personalize:::recipe/aws-vod-because-you-watched-x
- arn:aws:personalize:::recipe/aws-vod-more-like-x
- arn:aws:personalize:::recipe/aws-vod-top-picks
- arn:aws:personalize:::recipe/aws-ecomm-popular-items-by-views
- arn:aws:personalize:::recipe/aws-ecomm-popular-items-by-purchases
- arn:aws:personalize:::recipe/aws-ecomm-frequently-bought-together
- arn:aws:personalize:::recipe/aws-ecomm-customers-who-viewed-x-also-viewed
- arn:aws:personalize:::recipe/aws-ecomm-recommended-for-you
料金体系の違い
ここでは現時点での算出の方法について説明しますので、最新の情報や具体的な金額については以下を参照ください。
https://aws.amazon.com/personalize/pricing/?nc1=h_ls
Custom
側- こちらが従来通りの料金体系です。
- この中でも、Recipe TypeがUSER_SEGMENTATIONのものはさらに別の料金ルールとなり、そちらについての説明は省略します。
項目 | 説明 |
---|---|
データ取り込み | S3からPersonalizeにアップロードされるデータに1GB単位で料金が発生します。 |
トレーニング | SolutionVersionを作成するトレーニング時間数に応じて1時間毎に料金が発生します。 |
リアルタイム推論 | TPS(1時間あたりのレコメンド取得数)によって料金が変わります。 計算には設定するMinimum provisioned TPSと実際のTPSのどちらか大きい方が使用されます。 Minimum provisioned TPSを超えるようなリクエストがあった場合、自動的にスケールアップされます。 |
バッチ推論 | レコメンデーション1000件あたりに料金が発生します。 |
Domain
側Custom
と比較してモデルのtrainingには料金が発生しません。- recommender作成時にモデルtraining後、リアルタイム推論が同時に作成されますので、recommender作成直後から時間単位で料金が発生します。
項目 | 説明 |
---|---|
データ取り込み | S3からPersonalizeにアップロードされるデータに1GB単位で料金が発生します。 |
データセット内のユーザー数 | Recommender作成後はrecommender1個あたり1時間毎に料金が発生し、 この料金はデータセットに存在するユーザー数によって異なります。 |
追加レコメンデーション | Recommender作成後は時間単位で料金が発生し、 1時間の一定のレコメンド数まではその料金で変わりませんが、 1時間内で一定数を超えると、追加レコメンデーションの料金が発生します。 |
実際にVIDEO_ON_DEMANDで構築してみた
Modules
import boto3
from boto3.session import Session
import json
import pathlib
import pandas as pd
import numpy as np
データ作成
- ml-test-small.zipのrating.csvを元にカラムを編集・追加します。
- カラム名はルールがあるので、それに沿ってリネームします。
EVENT_TYPE
が必須のフィールドとして必要なため、今回は乱数で生成します。
df_ratings = pd.read_csv("ml-latest-small/ratings.csv")
# columnsの書き換え
df_ratings = df_ratings.rename(columns={'userId': 'USER_ID', 'movieId': 'ITEM_ID', 'timestamp': 'TIMESTAMP'})
# ratingを削除
df_ratings = df_ratings.drop('rating', axis=1)
# EVENT_TYPEを生成
rns = np.random.RandomState(seed=777)
list_event_type = rns.choice(['Watch', 'Click'], size=len(df_ratings), replace=True, p=[0.8,0.2])
df_ratings['EVENT_TYPE'] = list_event_type
df_ratings.to_csv("ml-latest-small/ratings_mod.csv", index=False)
変数の事前定義
bucket_name = 'Your S3 bucket name'
import_uri = 'Your S3 file uri' # s3://bucket_name/file_path'
region_name = 'ap-northeast-1'
prefix = 'trial-20220222'
dataset_group_name = f'{prefix}-dataset-group'
schema_interaction_name = f'{prefix}-schema-interaction'
dataset_interaction_name = f'{prefix}-dataset-interaction'
import_job_interaction_name = f'{prefix}-import-job-interaction'
recommender_name = f'{prefix}-recommender-vod-most-popular'
iam_role_name = f'{prefix}-personalize-exection-role'
iam_custom_policy_name = f'{prefix}-personalize-execution-policy'
client_s3 = boto3.client('s3', region_name=region_name)
client_personalize = boto3.client('personalize', region_name=region_name)
client_personalize_runtime = boto3.client('personalize-runtime', region_name=region_name)
client_iam = boto3.client('iam', region_name=region_name)
S3 bucketの作成と設定
# create_bucket
location = {'LocationConstraint': region_name}
client_s3.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location)
# put_public_access_block
client_s3.put_public_access_block(
Bucket=bucket_name,
PublicAccessBlockConfiguration={
'BlockPublicAcls': True,
'IgnorePublicAcls': True,
'BlockPublicPolicy': True,
'RestrictPublicBuckets': True,
},
)
# create bucket_policy
bucket_policy = {
"Version": "2012-10-17",
"Id": "PersonalizeS3BucketAccessPolicy",
"Statement": [
{
"Sid": "PersonalizeS3BucketAccessPolicy",
"Effect": "Allow",
"Principal": {
"Service": "personalize.amazonaws.com"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
f"arn:aws:s3:::{bucket_name}",
f"arn:aws:s3:::{bucket_name}/*"
]
}
]
}
bucket_policy = json.dumps(bucket_policy)
# put_bucket_policy
client_s3.put_bucket_policy(
Bucket=bucket_name,
Policy=bucket_policy,
)
S3 bucketへのアップロード
interactions_csv = pathlib.Path('ml-latest-small/ratings_mod.csv')
response = client_s3.upload_file(
str(interactions_csv),
bucket_name,
str(interactions_csv),
)
Domain dataset group作成
- domain =
VIDEO_ON_DEMAND
とします。(未指定だとCustom
になります)
response_create_dataset_group = client_personalize.create_dataset_group(
name=dataset_group_name,
domain='VIDEO_ON_DEMAND'
)
Schema作成
- Interactionsのみをデータとして使います。
- こちらも、domain =
VIDEO_ON_DEMAND
とする必要があります。
schema_interaction = {
"type": "record",
"name": "Interactions",
"namespace": "com.amazonaws.personalize.schema",
"fields": [
{
"name": "USER_ID",
"type": "string"
},
{
"name": "ITEM_ID",
"type": "string"
},
{
"name": "TIMESTAMP",
"type": "long"
},
{
"name": "EVENT_TYPE",
"type": "string"
},
],
"version": "1.0"
}
schema_interaction = json.dumps(schema_interaction)
response_create_schema = client_personalize.create_schema(
name=schema_interaction_name,
schema=schema_interaction,
domain='VIDEO_ON_DEMAND',
)
Dataset作成
response_create_dataset = client_personalize.create_dataset(
name=dataset_interaction_name,
schemaArn=response_create_schema['schemaArn'],
datasetGroupArn=response_create_dataset_group['datasetGroupArn'],
datasetType='Interactions'
)
IAMロール作成
- まずはcustom policyの作成します。
iam_custom_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
f"arn:aws:s3:::{bucket_name}"
]
},
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": [
f"arn:aws:s3:::{bucket_name}/*"
]
}
]
}
response_create_policy = client_iam.create_policy(
PolicyName=iam_custom_policy_name,
PolicyDocument=json.dumps(iam_custom_policy_document),
)
- 次にroleを作成します。
assume_role_policy_document = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "personalize.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
create_role_response = client_iam.create_role(
RoleName = iam_role_name,
AssumeRolePolicyDocument = json.dumps(assume_role_policy_document)
)
client_iam.attach_role_policy(
RoleName = iam_role_name,
PolicyArn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
)
client_iam.attach_role_policy(
RoleName = iam_role_name,
PolicyArn = response_create_policy['Policy']['Arn']
)
response_get_role = client_iam.get_role(RoleName=iam_role_name)
Import job作成
- 作成した、IAM Roleを与える必要があります。
response_create_dataset_import_job = client_personalize.create_dataset_import_job(
jobName=import_job_interaction_name,
datasetArn=response_create_dataset['datasetArn'],
dataSource={
'dataLocation': import_uri
},
roleArn=response_get_role['Role']['Arn']
)
Recommender作成
- 今回レシピは、aws-vod-most-popularを使います。
response_create_recommender = client_personalize.create_recommender(
name=recommender_name,
datasetGroupArn=response_create_dataset_group['datasetGroupArn'],
recipeArn='arn:aws:personalize:::recipe/aws-vod-most-popular',
)
レコメンデーション取得
- get_recommendationsは、recommenderArnもしくは、solutionVersionArnを指定する形となります。
Domain
の場合は、recommenderArnを指定。Custom
の場合は、solutionVersionArnを指定。
response_get_recommendations = client_personalize_runtime.get_recommendations(
userId="1",
numResults=5,
recommenderArn=response_create_recommender['recommenderArn'],
)
- 結果は以下のようなjson形式となります。
{
"ResponseMetadata": {
"RequestId": "9e8ec5f8-4a68-46d2-849f-14432877395a",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"content-type": "application/json",
"date": "Mon, 21 Feb 2022 07:22:11 GMT",
"x-amzn-requestid": "9e8ec5f8-4a68-46d2-849f-14432877395a",
"content-length": "322",
"connection": "keep-alive"
},
"RetryAttempts": 0
},
"itemList": [
{
"itemId": "356"
},
{
"itemId": "296"
},
{
"itemId": "318"
},
{
"itemId": "593"
},
{
"itemId": "2571"
}
],
"recommendationId": "RID-b0c39904-e612-4158-92e3-a1ca3661d508"
}
Recommender削除
- Recommenderはcampaignを含んでいるため、稼働分だけ料金が発生します。
- 使用後、不要となったら削除をしてください。
response_delete_recommender = client_personalize.delete_recommender(
recommenderArn=response_create_recommender['recommenderArn']
)
その他の違い
metricsの取得について
Custom
は、SolutionVersionを作成した結果として、metricsの確認ができましたが、Domain
ではそれが実施できないようです。
もし必要な場合は、metrics計算用のスクリプトを書いて求めるか、Custom
側でレシピを作成する必要があります。
バッチジョブへの対応
Custom
はリアルタイム推論以外にも、S3のファイルを読み込んで、結果をS3に出力するバッチジョブを実行することができましたが、
現状Domain
は、バッチジョブには対応していないようですのでご注意ください。
バッチジョブが必要な場合は従来通りCustom
側でレシピを作成する必要があります。
モデルの最適化オプション
Custom
側には、モデルの最適化としてHPO(ハイパーパラメータ最適化)やAutoML(最良なモデルを選択)を使用可能でしたが、Domain
はRecommender作成時にPersonalize側にお任せする形となりますので、設定項目としては選択できませんのでご注意ください。
まとめ
Domain
では、従来のCustom
よりも簡単にリアルタイム推論まで構築できることがわかりました。用途にマッチする場合は構築までの時間が短くて済むのでとても良いと感じました。
必要な処理によっては、従来のCustom
を使う必要もありますので、用途に応じて選択する必要がありそうです。