TensorFlow : How To : æ°ãã Op ã追å ãã ï¼ç¿»è¨³/解説ï¼
翻訳 : (æ ª)ã¯ã©ã¹ãã£ãã ã»ã¼ã«ã¹ã¤ã³ãã©ã¡ã¼ã·ã§ã³
æ´æ°æ¥æ : 09/16/2017
ä½ææ¥æ : 03/04/2016
* æ¬ãã¼ã¸ã¯ãTensorFlow æ¬å®¶ãµã¤ãã® Extend – Adding a New Op ã翻訳ããä¸ã§
é©å®ãè£è¶³èª¬æãããã®ã§ãï¼
* (obsolete, ãªã³ã¯åã) æ¬ãã¼ã¸ã¯ãTensorFlow ã®æ¬å®¶ãµã¤ãã® How To – Adding a New Op ã翻訳ããä¸ã§
é©å®ãè£è¶³èª¬æãããã®ã§ãï¼
https://www.tensorflow.org/versions/master/how_tos/adding_an_op/index.html#adding-a-new-op
* ãµã³ãã«ã³ã¼ãã®åä½ç¢ºèªã¯ãã¦ããã¾ãããé©å®ãè¿½å æ¹å¤ãã¦ãã¾ãã
* ãèªç±ã«ãªã³ã¯ãå¼µã£ã¦é ãã¦ãã¾ãã¾ããããsales-info@classcat.com ã¾ã§ãä¸å ±ããã ããã¨å¬ããã§ãã
æ¢åã®ã©ã¤ãã©ãªã§ã«ãã¼ããã¦ããªãæ¼ç®ãçµã¿å
¥ããã (incorporate) ã®ã§ããã°ã
ã«ã¹ã¿ã Op ã使ãããã¨ãã¦ãã¾ããã«ã¹ã¿ã Op ãçµã¿å
¥ããããã«ã¯ã以ä¸ãå¿
è¦ã¨ãªãã§ããã :
- C++ ãã¡ã¤ã«ã®æ°ãã Op ãç»é²ãã¾ããOp ç»é²ã¯å®è£ ããã¯ç¬ç«ã§ãã©ã®ããã« Op ãå¼ã³åºããã (invoke) ãã® semantics ãè¨è¿°ãã¾ããä¾ãã°ããã㯠Op åãå®ç¾©ãã¦ããã®å ¥åºåãæå®ãã¾ãã
- Op ã C++ ã§å®è£ ãã¾ãããã®å®è£ 㯠“ã«ã¼ãã«” ã¨å¼ã°ããç°ãªãã¢ã¼ããã¯ãã£ï¼ä¾ãã° CPUãGPUï¼ãå ¥åºåã¿ã¤ãã®ããã«è¤æ°ã®ã«ã¼ãã«ããã£ã¦ããã¾ãã¾ããã
- ãªãã·ã§ã³ã§ãPython ã©ããã¼ã使ãã¾ãããã®ã©ããã¼ã¯ Op ã使ããããã® public API ã§ããããã©ã«ãã»ã©ããã¼ã¯ Op ç»é²ããçæãããããã¯ç´æ¥ä½¿ç¨ã§ãã¾ããã追å ãããã¨ãã§ãã¾ãã
- ãªãã·ã§ã³ã§ãOp ã®ããã®å¾é ãè¨ç®ãã颿°ãæ¸ãã¾ãã
- ãªãã·ã§ã³ã§ãOp ã®ããã®å ¥åºåå½¢ç¶ (shape) ãè¨è¿°ãã颿°ãæ¸ãã¾ããããã¯è²´æ¹ã® Op ã¨åä½ããããã®å½¢ç¶æ¨è«ãå¯è½ã«ãã¾ãã
- Op ããã¹ããã¾ããå ¸åçã«ã¯ Python ã§ãããè²´æ¹ãå¾é ãå®ç¾©ãããªãã°ãPython GradientChecker ã§ããããæ¤è¨¼ã§ãã¾ãã
Op ã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ãå®ç¾©ãã
TensorFlow ã·ã¹ãã ã«ç»é²ãããã¨ã§ Op ã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ãå®ç¾©ãã¾ããç»é²ã«ããã¦ã¯ãè²´æ¹ã® Op ã®ååããã®å ¥åï¼åã¨ååï¼ã¨åºåï¼åã¨ååï¼ããã㦠Op ãå¿ è¦ã¨ãããããããªã docstrings 㨠attrs ãæå®ãã¾ãã
ãããã©ã®ããã«åä½ããããè¦ãããã«ãint32s ã®ãã³ã½ã«ãåããæåã®è¦ç´ ãé¤ãã¦ã¼ãã«è¨å®ããããã³ã½ã«ã®ã³ãã¼ãåºåãã Op ã使ãããã¨ä»®å®ãã¾ãããããã¡ã¤ã« tensorflow/core/user_ops/zero_out.cc ã使ãã¦ãã®ãã㪠Op ã®ããã®ã¤ã³ã¿ã¼ãã§ã¤ã¹ãå®ç¾©ãã REGISTER_OP ãã¯ãã¸ã®å¼ã³åºãã追å ãã¾ãã
#include "tensorflow/core/framework/op.h"
REGISTER_OP("ZeroOut")
.Input("to_zero: int32")
.Output("zeroed: int32");
ãã® ZeroOut Op ã¯å ¥åã¨ã㦠32-bit æ´æ°ã®ä¸ã¤ã®ãã³ã½ã« to_zero ãåãã32-bit æ´æ°ã®ãã³ã½ã« zeroed ãåºåãã¾ãã
ãã¼ãã³ã°ã«ã¤ãã¦ã®ãã¼ã : Ops ã®ååã¯ä¸æ (unique) ã§ãã£ã¡ã«ã±ã¼ã¹ã§ããã¹ãã§ããã¢ã³ãã¼ã¹ã³ã¢ (_) ã§å§ã¾ãååã¯å é¨ã¦ã¼ã¹ã®ããã«äºç´ããã¦ãã¾ãã
Op ã®ããã®ã«ã¼ãã«ãå®è£ ãã
ã¤ã³ã¿ã¼ãã§ã¤ã¹ãå®ç¾©ããå¾ã¯ãOp ã®ä¸ã¤ã¾ãã¯ãã以ä¸ã®å®è£ ãæä¾ãã¾ãããããã®ã«ã¼ãã«ã使ããããã«ã¯ãOpKernel ãæ¡å¼µããã¯ã©ã¹ã使ãã¦Compute ã¡ã½ããããªã¼ãã¼ã©ã¤ããã¾ããCompute ã¡ã½ããã¯å OpKernelContext* ã®ä¸ã¤ã® context 弿°ãæä¾ããããããå ¥åºåãã³ã½ã«ã®ãããªæç¨ãªãã®ã«ã¢ã¯ã»ã¹ã§ãã¾ãã
ä¸ã§ä½æãããã¡ã¤ã«ã«ã«ã¼ãã«ã追å ãã¾ããã«ã¼ãã«ã¯ãã®ãããªãã®ã«è¦ããã§ããã:
#include "tensorflow/core/framework/op_kernel.h"
using namespace tensorflow;
class ZeroOutOp : public OpKernel {
public:
explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {}
void Compute(OpKernelContext* context) override {
// å
¥åãã³ã½ã«ãåå¾ãã¾ã
const Tensor& input_tensor = context->input(0);
auto input = input_tensor.flat<int32>();
// åºåãã³ã½ã«ã使ãã¾ã
Tensor* output_tensor = NULL;
OP_REQUIRES_OK(context, context->allocate_output(0, input_tensor.shape(),
&output_tensor));
auto output = output_tensor->template flat<int32>();
// åºåãã³ã½ã«ã®æåã®è¦ç´ 以å¤ã®å
¨ã¦ã¯ 0 ã«ã»ããããã¾ãã
const int N = input.size();
for (int i = 1; i
ã«ã¼ãã«ãå®è£
ããå¾ã¯ãããã TensorFlow ã·ã¹ãã ã§ç»é²ãã¾ããç»é²ã«ããã¦ã¯ãç°ãªãæç¸ (constraints)ãããã§ãã®ã«ã¼ãã«ãåä½ãã¾ãããæå®ãã¾ããä¾ãã°ãCPU ã®ããã«ä½ãããä¸ã¤ã®ã«ã¼ãã«ãããããã㦠GPU ã®ããã«å¥ã®ä¸ã¤ãæã¤ããããã¾ããã
ZeroOut op ã®ããã«ãããè¡ãªãã«ã¯ãzero_out.cc ã«æ¬¡ã追å ãã¾ã :
REGISTER_KERNEL_BUILDER(Name("ZeroOut").Device(DEVICE_CPU), ZeroOutOp);
Op ã©ã¤ãã©ãªããã«ããã
With TensorFlow ãã¤ããªã»ã¤ã³ã¹ãã¬ã¼ã·ã§ã³
è²´æ¹ã®ã·ã¹ãã ä¸ã§å©ç¨å¯è½ãª g++ ããã㯠clang ã®ãã㪠C++ ã³ã³ãã¤ã©ã§ zero_out.cc ãã³ã³ãã¤ã«ã§ããã¹ãã§ãããã¤ã㪠PIP ããã±ã¼ã¸ã¯ãOp ãã³ã³ãã¤ã«ããã®ã«å¿
è¦ãªããããã¡ã¤ã«ã¨ã©ã¤ãã©ãªãã·ã¹ãã åºæã®å ´æã«ã¤ã³ã¹ãã¼ã«ãã¾ããããããªãããTensorFlow python ã©ã¤ãã©ãªã¯ãããã¨ã©ã¤ãã©ãªã»ãã£ã¬ã¯ããªãããããåå¾ããããã®é¢æ° get_include 㨠get_lib ãæä¾ãã¾ãã
Ubuntu ãã·ã³ä¸ã®ãããã®é¢æ°ã®åºåãããã«ç¤ºãã¾ãã
$ python
>>> import tensorflow as tf
>>> tf.sysconfig.get_include()
'/usr/local/lib/python2.7/site-packages/tensorflow/include'
>>> tf.sysconfig.get_lib()
'/usr/local/lib/python2.7/site-packages/tensorflow/core'
>>>
g++ ãã¤ã³ã¹ãã¼ã«ããã¦ããã¨ä»®å®ãã¦ãããã«ãOp ããã¤ãããã¯ã©ã¤ãã©ãªã«ã³ã³ãã¤ã«ããããã«ä½¿ç¨ã§ããã³ãã³ãã®ã·ã¼ã¯ã¨ã³ã¹ã示ãã¾ãã
$ TF_INC=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_include())')
$ TF_LIB=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_lib())')
$ g++ -std=c++11 -shared zero_out.cc -o zero_out.so \
-I $TF_INC -l tensorflow_framework -L $TF_LIB \
-fPIC -Wl,-rpath $TF_LIB
With TensorFlow ã½ã¼ã¹ã»ã¤ã³ã¹ãã¬ã¼ã·ã§ã³
TensorFlow ã½ã¼ã¹ã»ã¤ã³ã¹ãã¼ã«ããã¦ããã®ã§ããã°ãOp ãã³ã³ãã¤ã«ããããã« TensorFlow ã®ãã«ãã·ã¹ãã ã使ç¨ã§ãã¾ããæ¬¡ã® Bazel ãã«ãã»ã«ã¼ã«ãæã¤ BUILD ãã¡ã¤ã«ã tensorflow/core/user_ops ãã£ã¬ã¯ããªã«é
ç½®ãã¦ãã ããã
cc_binary(
name = "zero_out.so",
srcs = ["zero_out.cc"],
linkopts = [
"-Wl,-Bsymbolic",
"-lm",
],
linkshared = 1,
linkstatic = 1,
deps = [
"//third_party/tensorflow/core:framework",
],
)
zero_out.so ããã«ãããããã«æ¬¡ã®ã³ãã³ããå®è¡ãã¾ãã
$ bazel build -c opt //tensorflow/core/user_ops:zero_out.so
Python ã§ Op ã使ç¨ãã
TensorFlow Python API 㯠ãã¤ãããã¯ã©ã¤ãã©ãªããã¼ãã㦠Op ã TensorFlow ãã¬ã¼ã ã¯ã¼ã¯ã§ç»é²ããããã« load_op_library 颿°ãæä¾ãã¾ããload_op_library 㯠Python ã¢ã¸ã¥ã¼ã«ãè¿ãããã㯠Op ã¸ã® Python ã©ããã¼ãå«ã¿ã¾ãããããã¦ãã²ã¨ãã³ Op ããã«ãããã°ãPython ãããããå®è¡ããããã«æ¬¡ãè¡ãªããã¨ãã§ãã¾ã :
import tensorflow as tf
zero_out_module = tf.load_op_library('zero_out.so')
with tf.Session(''):
zero_out_module.zero_out([[1, 2], [3, 4]]).eval()
# Prints
array([[1, 0],
[0, 0]], dtype=int32)
ãã¼ã : çæããã颿°ã¯ï¼PEP8 ã«å¿ããï¼snake_case åãä¸ãããã¾ãããã®ãããã op ã C++ ãã¡ã¤ã«ã§ ZeroOut ã¨å½åããã¦ããã®ã§ããã°ãpython 颿°ã¯ zero_out ã¨å¼ç§°ããã¾ãã
Op ã Python ã¢ã¸ã¥ã¼ã«ãã import å¯è½ãªæ¨æºé¢æ°ã¨ãã¦å©ç¨å¯è½ã«ããããã«ã¯ã
次ã®ããã« Python ã½ã¼ã¹ãã¡ã¤ã«ã§ load_op_library å¼ã³åºããæã¤ãã¨ã¯å¤åæç¨ã§ãããï¼zero_out_op_1.py ãè¦ã¦ãã ããï¼:
import tensorflow as tf
_zero_out_module = tf.load_op_library('zero_out_op_kernel_1.so')
zero_out = _zero_out_module.zero_out
åä½ãããã¨ãæ¤è¨¼ãã
Op ãæåçã«å®è£
ãããããæ¤è¨¼ããè¯ãæ¹æ³ã¯ãã®ããã®ãã¹ããæ¸ãã¨ã§ããæ¬¡ã®å
容ã§ãã¡ã¤ã« tensorflow/python/kernel_tests/zero_out_op_test.py ã使ãã¾ã :
import tensorflow as tf
class ZeroOutTest(tf.test.TestCase):
def testZeroOut(self):
zero_out_module = tf.load_op_library('zero_out.so')
with self.test_session():
result = zero_out_module.zero_out([5, 4, 3, 2, 1])
self.assertAllEqual(result.eval(), [5, 0, 0, 0, 0])
ããã¦ãã¹ããå®è¡ãã¾ã :
$ bazel test tensorflow/python:zero_out_op_test
妥彿§ç¢ºèª (Validation)
ä¸ã®ä¾ã¯ Op ãä»»æã®å½¢ç¶ã«é©ç¨ããããã¨ãä»®å®ãã¦ãã¾ãããããããã¯ã¿ã ãã«é©ç¨ãããã¨ãããã©ãã§ãããï¼ããã¯ä¸ã® OpKernel å®è£
ã«ãã§ãã¯ã追å ãããã¨ãæå³ãã¦ãã¾ãã
void Compute(OpKernelContext* context) override {
// å
¥åãã³ã½ã«ãåå¾ãã
const Tensor& input_tensor = context->input(0);
OP_REQUIRES(context, TensorShapeUtils::IsVector(input_tensor.shape()),
errors::InvalidArgument("ZeroOut expects a 1-D vector."));
// ...
}
ããã¯å
¥åããã¯ã¿ã§ãã㨠assert ããããããã§ãªããªãã° InvalidArgument ã¹ãã¼ã¿ã¹ãè¨å®ãã¦è¿ãã¾ããOP_REQUIRES ãã¯ã ã¯ï¼ã¤ã®å¼æ°ãåãã¾ã :
- ã³ã³ããã¹ã (context)ããã㯠OpKernelContext ã OpKernelConstruction ãã¤ã³ã¿ (tensorflow/core/framework/op_kernel.h åç §) ã§ãSetStatus() ã¡ã½ããã®ããã§ãã
- æ¡ä»¶ãä¾ãã°ããã³ã½ã«ã®å½¢ç¶ã確èªããããã®é¢æ°ã tensorflow/core/framework/tensor_shape.h ã«ããã¾ãã
- ã¨ã©ã¼èªèº«ããã㯠Status ãªãã¸ã§ã¯ãã§è¡¨ããã¾ããtensorflow/core/lib/core/status.h ãè¦ã¦ãã ãããStatus ã¯åï¼ãã°ãã° InvalidArgumentããããåãªã¹ãåç §ï¼ã¨ã¡ãã»ã¼ã¸ã®ä¸¡æ¹ãæã¡ã¾ããã¨ã©ã¼ãæ§ç¯ããããã®é¢æ°ã¯ tensorflow/core/lib/core/errors.h ã§è¦ã¤ããã§ãããã
ä»ã®é¸æè¢ã¨ãã¦ããããã颿°ããè¿ããã Status ãªãã¸ã§ã¯ããã¨ã©ã¼ãã©ãããã¹ããã¦ããããããªãã°ãããè¿ãããã®ã§ããã°ãOP_REQUIRES_OK ã使ãã¾ããããããã¯ã両æ¹ã¯ã¨ã©ã¼æã®é¢æ°ããè¿ãã¾ãã
Op ç»é²
Attrs
Ops 㯠attrs ãæã¦ã¾ãããã®å¤ã¯ Op ãã°ã©ãã«è¿½å ãããæã«è¨å®ããã¾ããããã㯠Op ãæ§æããããã«ä½¿ç¨ããã¦ãããã¦ãã®å¤ã¯ã«ã¼ãã«å®è£ ã®å é¨ã¨ Op ç»é²ã®å ¥åºåã®åã®ä¸ã®ä¸¡æ¹ã§ã¢ã¯ã»ã¹ã§ãã¾ããå¯è½ãªæã«ã¯ attr ã®ä»£ããã«å ¥åã使ç¨ãããã¨ã好ã¾ããã§ãã使 ãªãå ¥åãããæè»ã ããã§ãããããã¯feed ã使ç¨ããããã«è¨å®ããçãå ¨ã¦ã®ã¹ãããã夿´ã§ãã¾ããattr ã¯å ¥åã§ã¯ã§ããªãäºæã«å¯¾ãã¦ä½¿ç¨ããã¾ã : signatureï¼å ¥åºåã®æ°ãåï¼ã«å½±é¿ããã step-to-step ããã¯å¤æ´ã§ããªãä»»æã®æ§æã§ãã
Op ãç»é²ããæã«ãAttr ã¡ã½ããã使ç¨ãã¦ãã®ååã¨åãæå®ãããã¨ã§ attr ãå®ç¾©ãã¾ããããã¯æ¬¡ã®å½¢å¼ã®ä»æ§ãæå¾ ããã¾ã :
<name>: <attr-type-expr>
ããã§ <name> ã¯æåã§å§ã¾ããè±æ°åã¨ã¢ã³ãã¼ã¹ã³ã¢ãããªãã¾ãããã㦠<attr-type-expr> ã¯ä¸ã§è¨è¿°ãããå½¢å¼ã®åã®å¼ (type expression) ã§ãã
ä¾ãã°ãZeroOut Op ãã¦ã¼ã¶æå® index ã¨ã㦠preserve (ä¿å) ããå ´åã«ã¯ã0 çªç®ã®è¦ç´ ã®ã¿ã®ä»£ããã«ãOp ãæ¬¡ã®ããã«ç»é²ã§ãã¾ã :
REGISTER_OP("ZeroOut")
.Attr("preserve_index: int")
.Input("to_zero: int32")
.Output("zeroed: int32");
ããããã°è²´æ¹ã®ã«ã¼ãã«ã¯ãã®ã³ã³ã¹ãã©ã¯ã¿ã§ context ãã©ã¡ã¼ã¿ãéãã¦ãã® attr ã«ã¢ã¯ã»ã¹ãããã¨ãã§ãã¾ã:
class ZeroOutOp : public OpKernel {
public:
explicit ZeroOutOp(OpKernelConstruction* context) : OpKernel(context) {
// ä¿å (preserve) ããããã®å¤ã® index ãå¾ãã
OP_REQUIRES_OK(context,
context->GetAttr("preserve_index", &preserve_index_));
// preserve_index ãæ£å¤ã§ãããã¨ã確èªããã
OP_REQUIRES(context, preserve_index_ >= 0,
errors::InvalidArgument("Need preserve_index >= 0, got ",
preserve_index_));
}
void Compute(OpKernelContext* context) override {
// ...
}
private:
int preserve_index_;
};
ããã¦ãã㯠Compute ã¡ã½ããã§ä½¿ç¨ã§ãã¾ã :
void Compute(OpKernelContext* context) override {
// ...
// preserve_index is ãç¯å²ã«ãããã¨ã確èªããã
OP_REQUIRES(context, preserve_index_
徿¹äºææ§ ãå®ãããã«ã¯ãæ¢åã® op ã« attr ã追å ããæ ããã©ã«ãå¤ ãæå®ãã¹ãã§ã :
REGISTER_OP("ZeroOut")
.Attr("preserve_index: int = 0")
.Input("to_zero: int32")
.Output("zeroed: int32");
Attr å
attr ã«ããã¦ã¯æ¬¡ã®åããµãã¼ãããã¾ã:
string: ä»»æã®ãã¤ãå (Any sequence of bytes) ï¼UTF8 ã§ããå¿ è¦ã¯ãªãï¼ãint: 符å·ä»ãæ´æ° (signed integer)float: æµ®åå°æ°ç¹æ°å¤ãbool: True ã¾ã㯠false.type: One of the (non-ref) values ofDataType.shape: ATensorShapeProto.tensor: ATensorProto.list(<type>): A list of<type>, where<type>is one of the above types.
Note thatlist(list(<type>))is invalid.
See also: op_def_builder.cc:FinalizeAttr for a definitive list.
Default values & constraints
Attrs may have default values, and some types of attrs can have constraints. To
define an attr with constraints, you can use the following <attr-type-expr>s:
-
{'<string1>', '<string2>'}: The value must be a string that has either the
value<string1>or<string2>. The name of the type,string, is implied
when you use this syntax. This emulates an enum:REGISTER_OP("EnumExample") .Attr("e: {'apple', 'orange'}"); -
{<type1>, <type2>}: The value is of typetype, and must be one of<type1>or<type2>, where<type1>and<type2>are supported
tensor types. You don't specify
that the type of the attr istype. This is implied when you have a list of
types in{...}. For example, in this case the attrtis a type that must
be anint32, afloat, or abool:REGISTER_OP("RestrictedTypeExample") .Attr("t: {int32, float, bool}"); -
There are shortcuts for common type constraints:
numbertype: Typetyperestricted to the numeric (non-string and
non-bool) types.realnumbertype: Likenumbertypewithout complex types.quantizedtype: Likenumbertypebut just the quantized number types.
The specific lists of types allowed by these are defined by the functions
(likeNumberTypes()) intensorflow/core/framework/types.h.
In this example the attrtmust be one of the numeric types:REGISTER_OP("NumberType") .Attr("t: numbertype");For this op:
tf.number_type(t=tf.int32) # Valid tf.number_type(t=tf.bool) # Invalid -
int >= <n>: The value must be an int whose value is greater than or equal to<n>, where<n>is a natural number.For example, the following Op registration specifies that the attr
amust
have a value that is at least2:REGISTER_OP("MinIntExample") .Attr("a: int >= 2"); -
list(<type>) >= <n>: A list of type<type>whose length is greater than
or equal to<n>.For example, the following Op registration specifies that the attr
ais a
list of types (eitherint32orfloat), and that there must be at least 3
of them:REGISTER_OP("TypeListExample") .Attr("a: list({int32, float}) >= 3");
To set a default value for an attr (making it optional in the generated code),
add = <default> to the end, as in:
REGISTER_OP("AttrDefaultExample")
.Attr("i: int = 0");
The supported syntax of the default value is what would be used in the proto
representation of the resulting GraphDef definition.
Here are examples for how to specify a default for all types:
REGISTER_OP("AttrDefaultExampleForAllTypes")
.Attr("s: string = 'foo'")
.Attr("i: int = 0")
.Attr("f: float = 1.0")
.Attr("b: bool = true")
.Attr("ty: type = DT_INT32")
.Attr("sh: shape = { dim { size: 1 } dim { size: 2 } }")
.Attr("te: tensor = { dtype: DT_INT32 int_val: 5 }")
.Attr("l_empty: list(int) = []")
.Attr("l_int: list(int) = [2, 3, 5, 7]");
Note in particular that the values of type type use the DT_* names
for the types.
以ä¸