شناسایی، ردیابی و طبقه بندی اشیاء با یک مدل طبقه بندی سفارشی در iOS

شما می‌توانید از کیت ML برای تشخیص و ردیابی اشیاء در فریم‌های ویدیویی متوالی استفاده کنید.

وقتی تصویری را به کیت ML ارسال می‌کنید، کیت تا پنج شیء را در تصویر به همراه موقعیت هر شیء در تصویر تشخیص می‌دهد. هنگام تشخیص اشیاء در جریان‌های ویدیویی، هر شیء یک شناسه منحصر به فرد دارد که می‌توانید از آن برای ردیابی شیء از فریم به فریم استفاده کنید.

شما می‌توانید از یک مدل طبقه‌بندی تصویر سفارشی برای طبقه‌بندی اشیاء شناسایی‌شده استفاده کنید. برای راهنمایی در مورد الزامات سازگاری مدل، محل یافتن مدل‌های از پیش آموزش‌دیده و نحوه آموزش مدل‌های خود، به مدل‌های سفارشی با کیت ML مراجعه کنید.

دو راه برای ادغام یک مدل سفارشی وجود دارد. می‌توانید مدل را با قرار دادن آن در پوشه asset برنامه خود، به صورت دسته‌ای (bundle) درآورید، یا می‌توانید آن را به صورت پویا از Cloud Storage دانلود کنید. جدول زیر این دو گزینه را با هم مقایسه می‌کند.

مدل بسته‌ای مدل میزبانی شده
این مدل بخشی از فایل .ipa برنامه شماست که باعث افزایش حجم آن می‌شود. این مدل بخشی از فایل .ipa برنامه شما نیست. با آپلود در فضای ابری (Cloud Storage) میزبانی می‌شود. توصیه می‌کنیم از فضای ابری برای فایربیس (Firebase) استفاده کنید.
این مدل بلافاصله در دسترس است، حتی زمانی که دستگاه اندروید آفلاین باشد برنامه شما باید شامل کدی باشد که مدل را بر اساس تقاضا دانلود کند
نیازی به پروژه Firebase نیست به یک پروژه Firebase نیاز دارد (در صورت استفاده از Cloud Storage برای Firebase).
برای به‌روزرسانی مدل، باید برنامه خود را دوباره منتشر کنید به‌روزرسانی‌های مدل را بدون انتشار مجدد برنامه خود، ارسال کنید
تست A/B داخلی ندارد تست A/B با پیکربندی از راه دور Firebase

امتحانش کن.

قبل از اینکه شروع کنی

  1. کتابخانه‌های ML Kit را در Podfile خود قرار دهید:

    pod 'GoogleMLKit/ObjectDetectionCustom', '8.0.0'
    
  2. پس از نصب یا به‌روزرسانی Pods پروژه خود، پروژه Xcode خود را با استفاده از .xcworkspace ‎ آن باز کنید. ML Kit در Xcode نسخه ۱۳.۲.۱ یا بالاتر پشتیبانی می‌شود.

  3. اگر می‌خواهید مدلی را با استفاده از فضای ذخیره‌سازی ابری برای فایربیس دانلود کنید ، اگر قبلاً فایربیس را به پروژه iOS خود اضافه نکرده‌اید، حتماً آن را اضافه کنید . این کار هنگام بسته‌بندی مدل لازم نیست.

۱. مدل را بارگذاری کنید

پیکربندی یک منبع مدل محلی

برای اتصال مدل به برنامه خود:

  1. فایل مدل (که معمولاً به .tflite یا .lite ختم می‌شود) را در پروژه Xcode خود کپی کنید، و هنگام انجام این کار، حتماً گزینه Copy bundle resources انتخاب کنید. فایل مدل در بسته برنامه قرار می‌گیرد و در ML Kit در دسترس خواهد بود.

  2. ایجاد شیء LocalModel ، با مشخص کردن مسیر فایل مدل:

    سویفت

    let localModel = LocalModel(path: localModelFilePath)

    هدف-سی

    MLKLocalModel *localModel =
        [[MLKLocalModel alloc] initWithPath:localModelFilePath];

پیکربندی یک منبع مدل میزبانی شده از راه دور

برای استفاده از مدل میزبانی‌شده از راه دور، باید فایل مدل را با استفاده از منطق برنامه خود در حافظه محلی دستگاه دانلود کنید و سپس آن را به عنوان یک مدل محلی بارگذاری کنید. توصیه می‌کنیم برای میزبانی یک مدل از فضای ذخیره‌سازی ابری برای فایربیس استفاده کنید. برای جزئیات پیاده‌سازی، به راهنمای مهاجرت از فایربیس ML به فضای ذخیره‌سازی ابری مراجعه کنید.

۲. آشکارساز شیء را پیکربندی کنید

پس از پیکربندی منابع مدل خود، آشکارساز شیء را برای مورد استفاده خود با یک شیء CustomObjectDetectorOptions پیکربندی کنید. می‌توانید تنظیمات زیر را تغییر دهید:

تنظیمات آشکارساز شیء
حالت تشخیص STREAM_MODE (پیش‌فرض) | SINGLE_IMAGE_MODE

در STREAM_MODE (پیش‌فرض)، آشکارساز شیء با تأخیر کم اجرا می‌شود، اما ممکن است در چند فراخوانی اول آشکارساز، نتایج ناقصی (مانند کادرهای محدودکننده نامشخص یا برچسب‌های دسته‌بندی) تولید کند. همچنین، در STREAM_MODE ، آشکارساز شناسه‌های ردیابی را به اشیاء اختصاص می‌دهد که می‌توانید از آنها برای ردیابی اشیاء در فریم‌ها استفاده کنید. از این حالت زمانی استفاده کنید که می‌خواهید اشیاء را ردیابی کنید یا زمانی که تأخیر کم مهم است، مانند پردازش جریان‌های ویدیویی در زمان واقعی.

در SINGLE_IMAGE_MODE ، آشکارساز شیء نتیجه را پس از تعیین محدوده شیء برمی‌گرداند. اگر طبقه‌بندی را نیز فعال کنید، نتیجه را پس از در دسترس بودن محدوده و برچسب دسته‌بندی برمی‌گرداند. در نتیجه، تأخیر تشخیص به طور بالقوه بیشتر است. همچنین، در SINGLE_IMAGE_MODE ، شناسه‌های ردیابی اختصاص داده نمی‌شوند. اگر تأخیر حیاتی نیست و نمی‌خواهید با نتایج جزئی سر و کار داشته باشید، از این حالت استفاده کنید.

تشخیص و ردیابی چندین شیء false (پیش‌فرض) | true

اینکه آیا تا پنج شیء شناسایی و ردیابی شود یا فقط برجسته‌ترین شیء (پیش‌فرض).

طبقه‌بندی اشیاء false (پیش‌فرض) | true

آیا اشیاء شناسایی شده با استفاده از مدل طبقه‌بندی سفارشی ارائه شده طبقه‌بندی شوند یا خیر. برای استفاده از مدل طبقه‌بندی سفارشی خود، باید این را روی true تنظیم کنید.

آستانه اطمینان طبقه‌بندی

حداقل امتیاز اطمینان برچسب‌های شناسایی‌شده. در صورت عدم تنظیم، از هر آستانه طبقه‌بندی‌کننده‌ای که توسط فراداده مدل مشخص شده باشد، استفاده خواهد شد. اگر مدل حاوی هیچ فراداده‌ای نباشد یا فراداده آستانه طبقه‌بندی‌کننده‌ای را مشخص نکند، از آستانه پیش‌فرض ۰.۰ استفاده خواهد شد.

حداکثر برچسب برای هر شیء

حداکثر تعداد برچسب‌هایی که آشکارساز به ازای هر شیء برمی‌گرداند. اگر تنظیم نشود، مقدار پیش‌فرض ۱۰ استفاده خواهد شد.

اگر فقط یک مدلِ بسته‌بندی‌شده‌ی محلی دارید، کافیست یک آشکارساز شیء از شیء LocalModel خود ایجاد کنید:

سویفت

let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3

هدف-سی

MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;

اگر مدلی دارید که از راه دور میزبانی می‌شود، باید قبل از اجرای آن، بررسی کنید که دانلود شده باشد.

اگرچه شما فقط باید قبل از اجرای آشکارساز شیء این را تأیید کنید، اگر هم یک مدل میزبانی از راه دور و هم یک مدل بسته‌بندی شده محلی دارید، انجام این بررسی هنگام نمونه‌سازی ObjectDetector منطقی است: اگر مدل از راه دور دانلود شده است، یک آشکارساز از آن و در غیر این صورت از مدل محلی ایجاد کنید.

سویفت

// Path where your download logic saves the model
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")

let model: LocalModel
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Use the downloaded model
  model = LocalModel(path: localModelURL.path)
} else {
  // Fall back to bundled model
  guard let bundledModelPath = Bundle.main.path(forResource: "model", ofType: "tflite") else { return }
  model = LocalModel(path: bundledModelPath)
}

let options = CustomObjectDetectorOptions(localModel: model)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3
let objectDetector = ObjectDetector.objectDetector(options: options)

هدف-سی

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];

MLKLocalModel *model;
if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Use the downloaded model
  model = [[MLKLocalModel alloc] initWithPath:localModelPath];
} else {
  // Fall back to bundled model
  NSString *bundledModelPath = [NSBundle.mainBundle pathForResource:@"model" ofType:@"tflite"];
  model = [[MLKLocalModel alloc] initWithPath:bundledModelPath];
}

MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:model];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;
MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];

اگر فقط یک مدل میزبانی‌شده از راه دور دارید، باید عملکردهای مربوط به مدل را غیرفعال کنید - مثلاً بخشی از رابط کاربری خود را خاکستری کنید یا پنهان کنید - تا زمانی که تأیید کنید مدل دانلود شده است.

سویفت

let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Model is already cached, initialize immediately
  self.initializeDetector(with: localModelURL)
} else {
  // Model is not yet available, show loading UI and start download
  self.showLoadingUI()
  let storage = Storage.storage()
  let modelRef = storage.reference(forURL: "gs://YOUR_BUCKET/path/to/model.tflite")
  modelRef.write(toFile: localModelURL) { url, error in
    self.hideLoadingUI()
    if let error = error {
      // Handle download error
      self.showErrorUI()
    } else if let modelURL = url {
      // Download success, initialize detector
      self.initializeDetector(with: modelURL)
    }
  }
}

func initializeDetector(with modelURL: URL) {
  let localModel = LocalModel(path: modelURL.path)
  let options = CustomObjectDetectorOptions(localModel: localModel)
  options.detectorMode = .singleImage
  options.shouldEnableClassification = true
  options.shouldEnableMultipleObjects = true
  self.objectDetector = ObjectDetector.objectDetector(options: options)
  // Enable ML features in UI
  self.enableMLFeatures()
}

هدف-سی

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];
NSURL *localModelURL = [NSURL fileURLWithPath:localModelPath];

if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Model is already cached, initialize immediately
  [self initializeDetectorWithURL:localModelURL];
} else {
  // Model is not yet available, show loading UI and start download
  [self showLoadingUI];

  FIRStorage *storage = [FIRStorage storage];
  FIRStorageReference *modelRef = [storage referenceForURL:@"gs://YOUR_BUCKET/path/to/model.tflite"];

  [modelRef writeToFile:localModelURL
             completion:^(NSURL * _Nullable URL, NSError * _Nullable error) {
               [self hideLoadingUI];
               if (error != nil) {
                 // Handle download error
                 [self showErrorUI];
               } else {
                 // Download success, initialize detector
                 [self initializeDetectorWithURL:URL];
               }
             }];
}

- (void)initializeDetectorWithURL:(NSURL *)modelURL {
  MLKLocalModel *localModel = [[MLKLocalModel alloc] initWithPath:modelURL.path];
  MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
  options.detectorMode = MLKObjectDetectorModeSingleImage;
  options.shouldEnableClassification = YES;
  options.shouldEnableMultipleObjects = YES;
  self.objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];

  // Enable ML features in UI
  [self enableMLFeatures];
}

API تشخیص و ردیابی اشیا برای این دو مورد استفاده اصلی بهینه شده است:

  • تشخیص و ردیابی زنده برجسته‌ترین شیء در منظره‌یاب دوربین.
  • تشخیص چندین شیء از یک تصویر ثابت

برای پیکربندی API برای این موارد استفاده:

سویفت

// Live detection and tracking
let options = CustomObjectDetectorOptions(localModel: localModel)
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

// Multiple object detection in static images
let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableMultipleObjects = true
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

هدف-سی

// Live detection and tracking
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

// Multiple object detection in static images
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableMultipleObjects = YES;
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

۳. تصویر ورودی را آماده کنید

با استفاده از UIImage یا CMSampleBuffer یک شیء VisionImage ایجاد کنید.

اگر از UIImage استفاده می‌کنید، مراحل زیر را دنبال کنید:

  • یک شیء VisionImage با UIImage ایجاد کنید. مطمئن شوید که .orientation صحیح را مشخص می‌کنید.

    سویفت

    let image = VisionImage(image: UIImage)
    visionImage.orientation = image.imageOrientation

    هدف-سی

    MLKVisionImage *visionImage = [[MLKVisionImage alloc] initWithImage:image];
    visionImage.orientation = image.imageOrientation;

اگر از CMSampleBuffer استفاده می‌کنید، مراحل زیر را دنبال کنید:

  • جهت داده‌های تصویر موجود در CMSampleBuffer را مشخص کنید.

    برای بدست آوردن جهت تصویر:

    سویفت

    func imageOrientation(
      deviceOrientation: UIDeviceOrientation,
      cameraPosition: AVCaptureDevice.Position
    ) -> UIImage.Orientation {
      switch deviceOrientation {
      case .portrait:
        return cameraPosition == .front ? .leftMirrored : .right
      case .landscapeLeft:
        return cameraPosition == .front ? .downMirrored : .up
      case .portraitUpsideDown:
        return cameraPosition == .front ? .rightMirrored : .left
      case .landscapeRight:
        return cameraPosition == .front ? .upMirrored : .down
      case .faceDown, .faceUp, .unknown:
        return .up
      }
    }
          

    هدف-سی

    - (UIImageOrientation)
      imageOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation
                             cameraPosition:(AVCaptureDevicePosition)cameraPosition {
      switch (deviceOrientation) {
        case UIDeviceOrientationPortrait:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationLeftMirrored
                                                                : UIImageOrientationRight;
    
        case UIDeviceOrientationLandscapeLeft:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationDownMirrored
                                                                : UIImageOrientationUp;
        case UIDeviceOrientationPortraitUpsideDown:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationRightMirrored
                                                                : UIImageOrientationLeft;
        case UIDeviceOrientationLandscapeRight:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationUpMirrored
                                                                : UIImageOrientationDown;
        case UIDeviceOrientationUnknown:
        case UIDeviceOrientationFaceUp:
        case UIDeviceOrientationFaceDown:
          return UIImageOrientationUp;
      }
    }
          
  • با استفاده از شیء و جهت‌گیری CMSampleBuffer یک شیء VisionImage ایجاد کنید:

    سویفت

    let image = VisionImage(buffer: sampleBuffer)
    image.orientation = imageOrientation(
      deviceOrientation: UIDevice.current.orientation,
      cameraPosition: cameraPosition)

    هدف-سی

     MLKVisionImage *image = [[MLKVisionImage alloc] initWithBuffer:sampleBuffer];
     image.orientation =
       [self imageOrientationFromDeviceOrientation:UIDevice.currentDevice.orientation
                                    cameraPosition:cameraPosition];

۴. ایجاد و اجرای آشکارساز شیء

  1. یک آشکارساز شیء جدید ایجاد کنید:

    سویفت

    let objectDetector = ObjectDetector.objectDetector(options: options)

    هدف-سی

    MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];
  2. سپس، از آشکارساز استفاده کنید:

    ناهمزمان:

    سویفت

    objectDetector.process(image) { objects, error in
        guard error == nil, let objects = objects, !objects.isEmpty else {
            // Handle the error.
            return
        }
        // Show results.
    }

    هدف-سی

    [objectDetector
        processImage:image
          completion:^(NSArray *_Nullable objects,
                       NSError *_Nullable error) {
            if (objects.count == 0) {
                // Handle the error.
                return;
            }
            // Show results.
         }];

    همزمان:

    سویفت

    var objects: [Object]
    do {
        objects = try objectDetector.results(in: image)
    } catch let error {
        // Handle the error.
        return
    }
    // Show results.

    هدف-سی

    NSError *error;
    NSArray *objects =
        [objectDetector resultsInImage:image error:&error];
    // Show results or handle the error.

۵. اطلاعات مربوط به اشیاء برچسب‌گذاری شده را دریافت کنید

اگر فراخوانی پردازشگر تصویر با موفقیت انجام شود، بسته به اینکه متد ناهمزمان یا همزمان را فراخوانی کرده باشید، یا لیستی از Object s) را به کنترل‌کننده‌ی تکمیل ارسال می‌کند یا لیست را برمی‌گرداند.

هر Object شامل ویژگی‌های زیر است:

frame یک CGRect که موقعیت شیء را در تصویر نشان می‌دهد.
trackingID یک عدد صحیح که شیء را در تصاویر مشخص می‌کند، یا `nil` در SINGLE_IMAGE_MODE.
labels
label.text توضیحات متنی برچسب. فقط در صورتی برگردانده می‌شود که فراداده‌های مدل LiteRT شامل توضیحات برچسب باشند.
label.index شاخص برچسب در میان تمام برچسب‌های پشتیبانی‌شده توسط طبقه‌بندی‌کننده.
label.confidence مقدار اطمینان طبقه‌بندی شیء.

سویفت

// objects contains one item if multiple object detection wasn't enabled.
for object in objects {
  let frame = object.frame
  let trackingID = object.trackingID
  let description = object.labels.enumerated().map { (index, label) in
    "Label \(index): \(label.text), \(label.confidence), \(label.index)"
  }.joined(separator: "\n")
}

هدف-سی

// The list of detected objects contains one item if multiple object detection
// wasn't enabled.
for (MLKObject *object in objects) {
  CGRect frame = object.frame;
  NSNumber *trackingID = object.trackingID;
  for (MLKObjectLabel *label in object.labels) {
    NSString *labelString =
        [NSString stringWithFormat:@"%@, %f, %lu",
                                   label.text,
                                   label.confidence,
                                   (unsigned long)label.index];
  }
}

تضمین یک تجربه کاربری عالی

برای بهترین تجربه کاربری، این دستورالعمل‌ها را در برنامه خود دنبال کنید:

  • تشخیص موفقیت‌آمیز اشیاء به پیچیدگی بصری شیء بستگی دارد. برای تشخیص، اشیاء با تعداد کمی از ویژگی‌های بصری ممکن است نیاز داشته باشند که بخش بیشتری از تصویر را اشغال کنند. شما باید کاربران را در مورد دریافت ورودی که با نوع اشیاء مورد نظر شما مطابقت دارد، راهنمایی کنید.
  • وقتی از طبقه‌بندی استفاده می‌کنید، اگر می‌خواهید اشیایی را که به طور کامل در دسته‌های پشتیبانی‌شده قرار نمی‌گیرند، شناسایی کنید، برای اشیای ناشناخته، روش‌های خاصی را پیاده‌سازی کنید.

همچنین، اپلیکیشن ویترین ML Kit Material Design و مجموعه ویژگی‌های Material Design Patterns for machine-powered را بررسی کنید.

بهبود عملکرد

اگر می‌خواهید از تشخیص اشیا در یک برنامه‌ی بلادرنگ استفاده کنید، برای دستیابی به بهترین نرخ فریم، این دستورالعمل‌ها را دنبال کنید:

  • وقتی از حالت استریمینگ در یک برنامه‌ی بلادرنگ استفاده می‌کنید، از تشخیص چند شیء استفاده نکنید، زیرا اکثر دستگاه‌ها قادر به تولید فریم‌ریت کافی نخواهند بود.

  • برای پردازش فریم‌های ویدیویی، از API همزمان results(in:) آشکارساز استفاده کنید. این متد را از تابع captureOutput(_, didOutput:from:) مربوط به AVCaptureVideoDataOutputSampleBufferDelegate فراخوانی کنید تا نتایج از فریم ویدیویی داده شده به صورت همزمان دریافت شوند. alwaysDiscardsLateVideoFrames مربوط به AVCaptureVideoDataOutput را برای فراخوانی‌های throttle به آشکارساز، true نگه دارید. اگر فریم ویدیویی جدیدی در حین اجرای آشکارساز در دسترس قرار گیرد، حذف خواهد شد.
  • اگر از خروجی آشکارساز برای همپوشانی گرافیک روی تصویر ورودی استفاده می‌کنید، ابتدا نتیجه را از کیت ML دریافت کنید، سپس تصویر و همپوشانی را در یک مرحله رندر کنید. با انجام این کار، برای هر فریم ورودی پردازش شده، فقط یک بار روی سطح نمایشگر رندر می‌کنید. برای مثال، به updatePreviewOverlayViewWithLastFrame در نمونه شروع سریع کیت ML مراجعه کنید.