<article class="syl-article-base tt-article-content syl-page-article syl-device-pc" style="box-sizing: border-box; display: block; padding: 0px; text-align: justify; overflow-wrap: break-word; word-break: break-word; overflow: hidden; hyphens: auto; color: rgb(34, 34, 34); font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", "Helvetica Neue", Arial, sans-serif; line-height: 1.667; font-size: 18px; margin-bottom: 20px;">
今天我們演示如何在生產(chǎn)環(huán)境中使用Keras笛园、Redis隘击、Flask 和 Apache 進(jìn)行深度學(xué)習(xí)
項目結(jié)構(gòu)
keras-complete-rest-api
├── helpers.py
├── jemma.png
├── keras_rest_api_app.wsgi
├── run_model_server.py
├── run_web_server.py
├── settings.py
├── simple_request.py
└── stress_test.py
文件解釋:
run_web_server.py 包含我們所有的 Flask Web 服務(wù)器代碼——Apache 將在啟動我們的深度學(xué)習(xí) Web 應(yīng)用程序時加載它侍芝。
run_model_server.py 將:
從磁盤加載我們的 Keras 模型
不斷輪詢Redis尋找新圖像進(jìn)行分類
對圖像進(jìn)行分類(批量處理以提高效率)
將推理結(jié)果寫回 Redis,以便它們可以通過 Flask 返回給客戶端埋同。
settings.py 包含我們深度學(xué)習(xí)生產(chǎn)服務(wù)的所有基于 Python 的設(shè)置州叠,例如 Redis 主機(jī)/端口信息、圖像分類設(shè)置凶赁、圖像隊列名稱等咧栗。
helpers.py 包含 run_web_server.py 和 run_model_server.py 都將使用的實用函數(shù)(即 base64 編碼)。
keras_rest_api_app.wsgi 包含我們的 WSGI 設(shè)置虱肄,因此我們可以從我們的 Apache 服務(wù)器為 Flask 應(yīng)用程序提供服務(wù)致板。
simple_request.py 可用于以編程方式使用我們的深度學(xué)習(xí) API 服務(wù)的結(jié)果。
jemma.png 是我家小獵犬的照片咏窿。在調(diào)用 REST API 以驗證它確實有效時斟或,我們將使用她作為示例圖像。
最后翰灾,我們將使用 stress_test.py 來給我們的服務(wù)器施加壓力并在整個過程中測量圖像分類缕粹。
我們在 Flask 服務(wù)器上有一個端點 /predict 。此方法位于 run_web_server.py 中纸淮,將根據(jù)需要計算輸入圖像的分類。圖像預(yù)處理也在 run_web_server.py 中處理亚享。
為了使我們的服務(wù)器做好生產(chǎn)準(zhǔn)備咽块,我從上周的單個腳本中取出了分類過程函數(shù)并將其放置在 run_model_server.py 中。這個腳本非常重要欺税,因為它將加載我們的 Keras 模型并從 Redis 中的圖像隊列中抓取圖像進(jìn)行分類侈沪。結(jié)果被寫回 Redis(/predict 端點和 run_web_server.py 中的相應(yīng)函數(shù)監(jiān)視 Redis 以將結(jié)果發(fā)送回客戶端)。
但是除非我們知道深度學(xué)習(xí) REST API 服務(wù)器的功能和局限性晚凿,否則它有什么好處呢亭罪?
在 stress_test.py 中,我們測試我們的服務(wù)器歼秽。我們將通過啟動 500 個并發(fā)線程來實現(xiàn)這一點应役,這些線程將我們的圖像發(fā)送到服務(wù)器進(jìn)行并行分類。我建議在服務(wù)器 localhost 上運行它以啟動燥筷,然后從異地客戶端運行它箩祥。
構(gòu)建我們的深度學(xué)習(xí)網(wǎng)絡(luò)應(yīng)用
圖 1:使用 Python、Keras谢揪、Redis 和 Flask 構(gòu)建的深度學(xué)習(xí) REST API 服務(wù)器的數(shù)據(jù)流圖蕉陋。
這個項目中使用的幾乎每一行代碼都來自我們之前關(guān)于構(gòu)建可擴(kuò)展深度學(xué)習(xí) REST API 的文章——唯一的變化是我們將一些代碼移動到單獨的文件中捐凭,以促進(jìn)生產(chǎn)環(huán)境中的可擴(kuò)展性。
設(shè)置和配置
# initialize Redis connection settings
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
# initialize constants used to control image spatial dimensions and
# data type
IMAGE_WIDTH = 224
IMAGE_HEIGHT = 224
IMAGE_CHANS = 3
IMAGE_DTYPE = "float32"
# initialize constants used for server queuing
IMAGE_QUEUE = "image_queue"
BATCH_SIZE = 32
SERVER_SLEEP = 0.25
CLIENT_SLEEP = 0.25
在 settings.py 中凳鬓,您將能夠更改服務(wù)器連接柑营、圖像尺寸 + 數(shù)據(jù)類型和服務(wù)器隊列的參數(shù)。
# import the necessary packages
import numpy as np
import base64
import sys
def base64_encode_image(a):
# base64 encode the input NumPy array
return base64.b64encode(a).decode("utf-8")
def base64_decode_image(a, dtype, shape):
# if this is Python 3, we need the extra step of encoding the
# serialized NumPy string as a byte object
if sys.version_info.major == 3:
a = bytes(a, encoding="utf-8")
# convert the string to a NumPy array using the supplied data
# type and target shape
a = np.frombuffer(base64.decodestring(a), dtype=dtype)
a = a.reshape(shape)
# return the decoded image
return a
helpers.py 文件包含兩個函數(shù)——一個用于 base64 編碼村视,另一個用于解碼官套。
編碼是必要的,以便我們可以在 Redis 中序列化 + 存儲我們的圖像蚁孔。 同樣奶赔,解碼是必要的,以便我們可以在預(yù)處理之前將圖像反序列化為 NumPy 數(shù)組格式杠氢。
深度學(xué)習(xí)網(wǎng)絡(luò)服務(wù)器
# import the necessary packages
from tensorflow.keras.preprocessing.image import img_to_array
from tensorflow.keras.applications.resnet50 import preprocess_input
from PIL import Image
import numpy as np
import settings
import helpers
import flask
import redis
import uuid
import time
import json
import io
# initialize our Flask application and Redis server
app = flask.Flask(__name__)
db = redis.StrictRedis(host=settings.REDIS_HOST,
port=settings.REDIS_PORT, db=settings.REDIS_DB)
def prepare_image(image, target):
# if the image mode is not RGB, convert it
if image.mode != "RGB":
image = image.convert("RGB")
# resize the input image and preprocess it
image = image.resize(target)
image = img_to_array(image)
image = np.expand_dims(image, axis=0)
image = preprocess_input(image)
# return the processed image
return image
@app.route("/")
def homepage():
return "Welcome to the PyImageSearch Keras REST API!"
@app.route("/predict", methods=["POST"])
def predict():
# initialize the data dictionary that will be returned from the
# view
data = {"success": False}
# ensure an image was properly uploaded to our endpoint
if flask.request.method == "POST":
if flask.request.files.get("image"):
# read the image in PIL format and prepare it for
# classification
image = flask.request.files["image"].read()
image = Image.open(io.BytesIO(image))
image = prepare_image(image,
(settings.IMAGE_WIDTH, settings.IMAGE_HEIGHT))
# ensure our NumPy array is C-contiguous as well,
# otherwise we won't be able to serialize it
image = image.copy(order="C")
# generate an ID for the classification then add the
# classification ID + image to the queue
k = str(uuid.uuid4())
image = helpers.base64_encode_image(image)
d = {"id": k, "image": image}
db.rpush(settings.IMAGE_QUEUE, json.dumps(d))
# keep looping until our model server returns the output
# predictions
while True:
# attempt to grab the output predictions
output = db.get(k)
# check to see if our model has classified the input
# image
if output is not None:
# add the output predictions to our data
# dictionary so we can return it to the client
output = output.decode("utf-8")
data["predictions"] = json.loads(output)
# delete the result from the database and break
# from the polling loop
db.delete(k)
break
# sleep for a small amount to give the model a chance
# to classify the input image
time.sleep(settings.CLIENT_SLEEP)
# indicate that the request was a success
data["success"] = True
# return the data dictionary as a JSON response
return flask.jsonify(data)
# for debugging purposes, it's helpful to start the Flask testing
# server (don't use this for production
if __name__ == "__main__":
print("* Starting web service...")
app.run()
在 run_web_server.py 中站刑,您將看到 predict ,該函數(shù)與我們的 REST API /predict 端點相關(guān)聯(lián)鼻百。
predict 函數(shù)將編碼的圖像推送到 Redis 隊列中绞旅,然后不斷循環(huán)/輪詢,直到它從模型服務(wù)器獲取預(yù)測數(shù)據(jù)温艇。 然后我們對數(shù)據(jù)進(jìn)行 JSON 編碼并指示 Flask 將數(shù)據(jù)發(fā)送回客戶端因悲。
深度學(xué)習(xí)模型服務(wù)器
# import the necessary packages
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.applications.resnet50 import decode_predictions
import numpy as np
import settings
import helpers
import redis
import time
import json
# connect to Redis server
db = redis.StrictRedis(host=settings.REDIS_HOST,
port=settings.REDIS_PORT, db=settings.REDIS_DB)
def classify_process():
# load the pre-trained Keras model (here we are using a model
# pre-trained on ImageNet and provided by Keras, but you can
# substitute in your own networks just as easily)
print("* Loading model...")
model = ResNet50(weights="imagenet")
print("* Model loaded")
# continually pool for new images to classify
while True:
# attempt to grab a batch of images from the database, then
# initialize the image IDs and batch of images themselves
queue = db.lrange(settings.IMAGE_QUEUE, 0,
settings.BATCH_SIZE - 1)
imageIDs = []
batch = None
# loop over the queue
for q in queue:
# deserialize the object and obtain the input image
q = json.loads(q.decode("utf-8"))
image = helpers.base64_decode_image(q["image"],
settings.IMAGE_DTYPE,
(1, settings.IMAGE_HEIGHT, settings.IMAGE_WIDTH,
settings.IMAGE_CHANS))
# check to see if the batch list is None
if batch is None:
batch = image
# otherwise, stack the data
else:
batch = np.vstack([batch, image])
# update the list of image IDs
imageIDs.append(q["id"])
# check to see if we need to process the batch
if len(imageIDs) > 0:
# classify the batch
print("* Batch size: {}".format(batch.shape))
preds = model.predict(batch)
results = decode_predictions(preds)
# loop over the image IDs and their corresponding set of
# results from our model
for (imageID, resultSet) in zip(imageIDs, results):
# initialize the list of output predictions
output = []
# loop over the results and add them to the list of
# output predictions
for (imagenetID, label, prob) in resultSet:
r = {"label": label, "probability": float(prob)}
output.append(r)
# store the output predictions in the database, using
# the image ID as the key so we can fetch the results
db.set(imageID, json.dumps(output))
# remove the set of images from our queue
db.ltrim(settings.IMAGE_QUEUE, len(imageIDs), -1)
# sleep for a small amount
time.sleep(settings.SERVER_SLEEP)
# if this is the main thread of execution start the model server
# process
if __name__ == "__main__":
classify_process()
run_model_server.py 文件包含我們的classify_process 函數(shù)。 這個函數(shù)加載我們的模型勺爱,然后對一批圖像運行預(yù)測晃琳。 這個過程最好在 GPU 上執(zhí)行,但也可以使用 CPU琐鲁。
在這個例子中卫旱,為了簡單起見,我們將使用在 ImageNet 數(shù)據(jù)集上預(yù)訓(xùn)練的 ResNet50围段。 您可以修改classify_process 以利用您自己的深度學(xué)習(xí)模型顾翼。
WSGI 配置
# add our app to the system path
import sys
sys.path.insert(0, "/var/www/html/keras-complete-rest-api")
# import the application and away we go...
from run_web_server import app as application
壓力測試
# import the necessary packages
from threading import Thread
import requests
import time
# initialize the Keras REST API endpoint URL along with the input
# image path
KERAS_REST_API_URL = "http://localhost/predict"
IMAGE_PATH = "jemma.png"
# initialize the number of requests for the stress test along with
# the sleep amount between requests
NUM_REQUESTS = 500
SLEEP_COUNT = 0.05
def call_predict_endpoint(n):
# load the input image and construct the payload for the request
image = open(IMAGE_PATH, "rb").read()
payload = {"image": image}
# submit the request
r = requests.post(KERAS_REST_API_URL, files=payload).json()
# ensure the request was sucessful
if r["success"]:
print("[INFO] thread {} OK".format(n))
# otherwise, the request failed
else:
print("[INFO] thread {} FAILED".format(n))
# loop over the number of threads
for i in range(0, NUM_REQUESTS):
# start a new thread to call the API
t = Thread(target=call_predict_endpoint, args=(i,))
t.daemon = True
t.start()
time.sleep(SLEEP_COUNT)
# insert a long sleep so we can wait until the server is finished
# processing the images
time.sleep(300)
我們的 stress_test.py 腳本將幫助我們測試服務(wù)器并確定其限制。 我總是建議對您的深度學(xué)習(xí) REST API 服務(wù)器進(jìn)行壓力測試奈泪,以便您知道是否(更重要的是适贸,何時)需要添加額外的 GPU、CPU 或 RAM段磨。 此腳本啟動 NUM_REQUESTS 線程和 POST 到 /predict 端點取逾。 這取決于我們的 Flask 網(wǎng)絡(luò)應(yīng)用程序。
編譯安裝Redis
Redis 是一種高效的內(nèi)存數(shù)據(jù)庫苹支,它將充當(dāng)我們的隊列/消息代理砾隅。 獲取和安裝Redis非常簡單:
$ wget http://download.redis.io/redis-stable.tar.gz
$ tar xvzf redis-stable.tar.gz
$ cd redis-stable
$ make
$ sudo make install
安裝 Apache Web 服務(wù)器
可以使用其他 Web 服務(wù)器,例如 nginx债蜜,但由于我對 Apache 有更多的經(jīng)驗(因此通常更熟悉 Apache)晴埂,因此我將在此示例中使用 Apache究反。 Apache 可以通過以下方式安裝:
$ sudo apt-get install apache2
如果您使用 Python 3 創(chuàng)建了一個虛擬環(huán)境,您將需要安裝 Python 3 WSGI + Apache 模塊:
$ sudo apt-get install libapache2-mod-wsgi-py3
$ sudo a2enmod wsgi
要驗證是否安裝了 Apache儒洛,請打開瀏覽器并輸入 Web 服務(wù)器的 IP 地址精耐。 如果您看不到服務(wù)器啟動畫面,請確保打開端口 80 和端口 5000琅锻。 就我而言卦停,我服務(wù)器的 IP 地址是 54.187.46.215(你的會有所不同)。 在瀏覽器中輸入這個恼蓬,我看到:
Sym-link鏈接您的 Flask + 深度學(xué)習(xí)應(yīng)用程序
默認(rèn)情況下小槐,Apache 提供來自 /var/www/html 的內(nèi)容。 我建議創(chuàng)建一個從 /var/www/html 到 Flask Web 應(yīng)用程序的符號鏈接荷辕。 我已將我的深度學(xué)習(xí) + Flask 應(yīng)用程序上傳到名為 keras-complete-rest-api 的目錄中的主目錄:
$ ls ~
keras-complete-rest-api
我可以通過以下方式將其符號鏈接到 /var/www/html:
$ cd /var/www/html/
$ sudo ln -s ~/keras-complete-rest-api keras-complete-rest-api
更新您的 Apache 配置以指向 Flask 應(yīng)用程序
為了將 Apache 配置為指向我們的 Flask 應(yīng)用程序凿跳,我們需要編輯
/etc/apache2/sites-available/000-default.conf 。 在你最喜歡的文本編輯器中打開(這里我將使用 vi ):
$ sudo vi /etc/apache2/sites-available/000-default.conf
在文件的頂部提供您的 WSGIPythonHome(Python bin 目錄的路徑)和 WSGIPythonPath(Python 站點包目錄的路徑)配置:
WSGIPythonHome /home/ubuntu/.virtualenvs/keras_flask/bin
WSGIPythonPath /home/ubuntu/.virtualenvs/keras_flask/lib/python3.5/site-packages
<VirtualHost *:80>
...
</VirtualHost>
在 Ubuntu 18.04 上疮方,您可能需要將第一行更改為:
WSGIPythonHome /home/ubuntu/.virtualenvs/keras_flask
由于我們在本示例中使用 Python 虛擬環(huán)境(我將我的虛擬環(huán)境命名為 keras_flask )控嗜,因此我們?yōu)?Python 虛擬環(huán)境提供 bin 和 site-packages 目錄的路徑。 然后在 的正文中案站,在 ServerAdmin 和 DocumentRoot 之后躬审,添加:
<VirtualHost *:80>
...
WSGIDaemonProcess keras_rest_api_app threads=10
WSGIScriptAlias / /var/www/html/keras-complete-rest-api/keras_rest_api_app.wsgi
<Directory /var/www/html/keras-complete-rest-api>
WSGIProcessGroup keras_rest_api_app
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
...
</VirtualHost>
符號鏈接 CUDA 庫(可選,僅限 GPU)
如果您將 GPU 用于深度學(xué)習(xí)并希望利用 CUDA(您為什么不這樣做)蟆盐,不幸的是,Apache 不了解 /usr/local/cuda/lib64 中的 CUDA 的 *.so 庫遭殉。
我不確定什么是“最正確”的方式向 Apache 指示這些 CUDA 庫所在的位置石挂,但“完全破解”解決方案是將所有文件從 /usr/local/cuda/lib64 符號鏈接到 /usr/lib :
$ cd /usr/lib
$ sudo ln -s /usr/local/cuda/lib64/* ./
重新啟動 Apache Web 服務(wù)器
編輯完 Apache 配置文件并可選擇符號鏈接 CUDA 深度學(xué)習(xí)庫后,請務(wù)必通過以下方式重新啟動 Apache 服務(wù)器:
$ sudo service apache2 restart
測試您的 Apache Web 服務(wù)器 + 深度學(xué)習(xí)端點
要測試 Apache 是否已正確配置以提供 Flask + 深度學(xué)習(xí)應(yīng)用程序险污,請刷新您的 Web 瀏覽器:
您現(xiàn)在應(yīng)該看到文本“歡迎使用 PyImageSearch Keras REST API拯腮!” 在您的瀏覽器中。 一旦你達(dá)到這個階段蚁飒,你的 Flask 深度學(xué)習(xí)應(yīng)用程序就應(yīng)該準(zhǔn)備好了动壤。 綜上所述,如果您遇到任何問題淮逻,請確保參考下一節(jié)……
提示:如果遇到問題琼懊,請監(jiān)控 Apache 錯誤日志
多年來阁簸,我一直在使用 Python + Web 框架,例如 Flask 和 Django哼丈,但在正確配置環(huán)境時仍然會出錯启妹。 雖然我希望有一種防彈的方法來確保一切順利,但事實是醉旦,在此過程中可能會出現(xiàn)一些問題饶米。 好消息是 WSGI 將 Python 事件(包括失敗)記錄到服務(wù)器日志中车胡。 在 Ubuntu 上檬输,Apache 服務(wù)器日志位于 /var/log/apache2/ :
$ ls /var/log/apache2
access.log error.log other_vhosts_access.log
調(diào)試時,我經(jīng)常打開一個運行的終端:
$ tail -f /var/log/apache2/error.log
......所以我可以看到第二個錯誤滾滾而來吨拍。 使用錯誤日志幫助您在服務(wù)器上啟動和運行 Flask褪猛。
啟動您的深度學(xué)習(xí)模型服務(wù)器
您的 Apache 服務(wù)器應(yīng)該已經(jīng)在運行。 如果沒有羹饰,您可以通過以下方式啟動它:
$ sudo service apache2 start
然后伊滋,您將要啟動 Redis 存儲:
$ redis-server
并在單獨的終端中啟動 Keras 模型服務(wù)器:
$ python run_model_server.py
* Loading model...
...
* Model loaded
從那里嘗試向您的深度學(xué)習(xí) API 服務(wù)提交示例圖像:
$ curl -X POST -F image=@jemma.png 'http://localhost/predict'
{
"predictions": [
{
"label": "beagle",
"probability": 0.9461532831192017
},
{
"label": "bluetick",
"probability": 0.031958963721990585
},
{
"label": "redbone",
"probability": 0.0066171870566904545
},
{
"label": "Walker_hound",
"probability": 0.003387963864952326
},
{
"label": "Greater_Swiss_Mountain_dog",
"probability": 0.0025766845792531967
}
],
"success": true
}
如果一切正常,您應(yīng)該會收到來自深度學(xué)習(xí) API 模型服務(wù)器的格式化 JSON 輸出队秩,其中包含類別預(yù)測 + 概率笑旺。
對您的深度學(xué)習(xí) REST API 進(jìn)行壓力測試
當(dāng)然,這只是一個例子鸟蟹。 讓我們對深度學(xué)習(xí) REST API 進(jìn)行壓力測試乌妙。 打開另一個終端并執(zhí)行以下命令:
$ python stress_test.py
[INFO] thread 3 OK
[INFO] thread 0 OK
[INFO] thread 1 OK
...
[INFO] thread 497 OK
[INFO] thread 499 OK
[INFO] thread 498 OK
在 run_model_server.py 輸出中,您將開始看到記錄到終端的以下行:
* Batch size: (4, 224, 224, 3)
* Batch size: (9, 224, 224, 3)
* Batch size: (9, 224, 224, 3)
* Batch size: (8, 224, 224, 3)
...
* Batch size: (2, 224, 224, 3)
* Batch size: (10, 224, 224, 3)
* Batch size: (7, 224, 224, 3)
即使每 0.05 秒有一個新請求建钥,我們的批次大小也不會超過每批次約 10-12 張圖像藤韵。 我們的模型服務(wù)器可以輕松處理負(fù)載而不會出汗,并且可以輕松擴(kuò)展到此之外熊经。 如果您確實使服務(wù)器超載(可能是您的批大小太大并且 GPU 內(nèi)存不足并顯示錯誤消息)泽艘,您應(yīng)該停止服務(wù)器,并使用 Redis CLI 清除隊列:
$ redis-cli
> FLUSHALL
從那里您可以調(diào)整 settings.py 和
/etc/apache2/sites-available/000-default.conf 中的設(shè)置镐依。 然后您可以重新啟動服務(wù)器匹涮。
將您自己的深度學(xué)習(xí)模型部署到生產(chǎn)環(huán)境的建議
我能給出的最好建議之一是將您的數(shù)據(jù),尤其是 Redis 服務(wù)器槐壳,靠近 GPU然低。 您可能想啟動一個具有數(shù)百 GB RAM 的巨型 Redis 服務(wù)器來處理多個圖像隊列并為多個 GPU 機(jī)器提供服務(wù)。 這里的問題將是 I/O 延遲和網(wǎng)絡(luò)開銷。
假設(shè) 224 x 224 x 3 圖像表示為 float32 數(shù)組脚翘,32 張圖像的批量大小將是 ~19MB 的數(shù)據(jù)灼卢。 這意味著對于來自模型服務(wù)器的每個批處理請求,Redis 將需要提取 19MB 的數(shù)據(jù)并將其發(fā)送到服務(wù)器来农。 在快速切換上鞋真,這沒什么大不了的,但您應(yīng)該考慮在同一臺服務(wù)器上同時運行模型服務(wù)器和 Redis沃于,以使數(shù)據(jù)靠近 GPU涩咖。
總結(jié)
在今天的博文中,我們學(xué)習(xí)了如何使用 Keras繁莹、Redis檩互、Flask 和 Apache 將深度學(xué)習(xí)模型部署到生產(chǎn)環(huán)境中。 我們在這里使用的大多數(shù)工具都是可以互換的咨演。您可以將 TensorFlow 或 PyTorch 換成 Keras闸昨。可以使用 Django 代替 Flask薄风。 Nginx 可以換成 Apache饵较。
我不建議換掉的唯一工具是 Redis。 Redis 可以說是內(nèi)存數(shù)據(jù)存儲的最佳解決方案遭赂。除非您有不使用 Redis 的特定原因循诉,否則我建議您使用 Redis 進(jìn)行排隊操作。 最后撇他,我們對深度學(xué)習(xí) REST API 進(jìn)行了壓力測試茄猫。
我們向我們的服務(wù)器提交了總共 500 個圖像分類請求,每個請求之間有 0.05 秒的延遲——我們的服務(wù)器沒有分階段(CNN 的批量大小從未超過約 37%)困肩。