| #include "caffe2/operators/apmeter_op.h" |
| |
| namespace caffe2 { |
| |
| template <> |
| void APMeterOp<float, CPUContext>::BufferPredictions( |
| const float* XData, |
| const int* labelData, |
| int N, |
| int D) { |
| if (buffers_.empty()) { |
| // Initialize the buffer |
| buffers_.resize(D, std::vector<BufferDataType>(buffer_size_)); |
| } |
| TORCH_DCHECK_EQ(buffers_.size(), D); |
| |
| // Fill atmose buffer_size_ data at a time, so truncate the input if needed |
| if (N > buffer_size_) { |
| XData = XData + (N - buffer_size_) * D; |
| labelData = labelData + (N - buffer_size_) * D; |
| N = buffer_size_; |
| } |
| |
| // Reclaim space if not enough space in the buffer to hold new data |
| int space_to_reclaim = buffer_used_ + N - buffer_size_; |
| if (space_to_reclaim > 0) { |
| for (auto& buffer : buffers_) { |
| std::rotate( |
| buffer.begin(), buffer.begin() + space_to_reclaim, buffer.end()); |
| } |
| buffer_used_ -= space_to_reclaim; |
| } |
| |
| // Fill the buffer |
| for (int i = 0; i < D; i++) { |
| for (int j = 0; j < N; j++) { |
| buffers_[i][buffer_used_ + j].first = XData[j * D + i]; |
| buffers_[i][buffer_used_ + j].second = labelData[j * D + i]; |
| } |
| } |
| |
| buffer_used_ += N; |
| } |
| |
| template <> |
| bool APMeterOp<float, CPUContext>::RunOnDevice() { |
| auto& X = Input(PREDICTION); |
| auto& label = Input(LABEL); |
| |
| // Check dimensions |
| TORCH_DCHECK_EQ(X.dim(), 2); |
| int N = X.dim32(0); |
| int D = X.dim32(1); |
| TORCH_DCHECK_EQ(label.dim(), 2); |
| TORCH_DCHECK_EQ(label.dim32(0), N); |
| TORCH_DCHECK_EQ(label.dim32(1), D); |
| auto* Y = Output(0, {D}, at::dtype<float>()); |
| |
| const auto* Xdata = X.data<float>(); |
| const auto* labelData = label.data<int>(); |
| auto* Ydata = Y->template mutable_data<float>(); |
| |
| BufferPredictions(Xdata, labelData, N, D); |
| |
| // Calculate AP for each class |
| for (int i = 0; i < D; i++) { |
| auto& buffer = buffers_[i]; |
| // Sort predictions by score |
| std::stable_sort( |
| buffer.begin(), |
| buffer.begin() + buffer_used_, |
| [](const BufferDataType& p1, const BufferDataType& p2) { |
| return p1.first > p2.first; |
| }); |
| // Calculate cumulative precision for each sample |
| float tp_sum = 0.0; |
| float precision_sum = 0.0; |
| int ntruth = 0; |
| for (int j = 0; j < buffer_used_; j++) { |
| tp_sum += buffer[j].second; |
| if (buffer[j].second == 1) { |
| ntruth += 1; |
| // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions,bugprone-narrowing-conversions) |
| precision_sum += tp_sum / (j + 1); |
| } |
| } |
| |
| // Calculate AP |
| Ydata[i] = precision_sum / std::max(1, ntruth); |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| REGISTER_CPU_OPERATOR(APMeter, APMeterOp<float, CPUContext>); |
| |
| OPERATOR_SCHEMA(APMeter) |
| .NumInputs(2) |
| .NumOutputs(1) |
| .ScalarType(TensorProto::FLOAT) |
| .SetDoc(R"DOC( |
| APMeter computes Average Precision for binary or multi-class classification. |
| It takes two inputs: prediction scores P of size (n_samples x n_classes), and |
| true labels Y of size (n_samples x n_classes). It returns a single float number |
| per class for the average precision of that class. |
| )DOC") |
| .Arg( |
| "buffer_size", |
| "(int32_t) indicates how many predictions should the op buffer. " |
| "defaults to 1000") |
| .Input( |
| 0, |
| "predictions", |
| "2-D tensor (Tensor<float>) of size (num_samples x" |
| "num_classes) containing prediction scores") |
| .Input( |
| 1, |
| "labels", |
| "2-D tensor (Tensor<float>) of size (num_samples) " |
| "containing true labels for each sample") |
| .Output( |
| 0, |
| "AP", |
| "1-D tensor (Tensor<float>) of size num_classes containing " |
| "average precision for each class"); |
| |
| SHOULD_NOT_DO_GRADIENT(APMeter); |
| |
| } // namespace |
| } // namespace caffe2 |