Browse Source

!889 UI mindInsight-explain add new features

From: @xia_yi_fan1
Reviewed-by: @ouwenchang,@ouwenchang
Signed-off-by:
tags/v1.1.0
mindspore-ci-bot Gitee 5 years ago
parent
commit
55d343e23f
9 changed files with 1157 additions and 391 deletions
  1. +1
    -1
      mindinsight/ui/src/components/benchmark-bar-chart.vue
  2. +325
    -0
      mindinsight/ui/src/components/radar-chart.vue
  3. +319
    -0
      mindinsight/ui/src/components/search-select.vue
  4. +4
    -0
      mindinsight/ui/src/components/select-group.vue
  5. +0
    -0
      mindinsight/ui/src/components/superpose-img.vue
  6. +12
    -5
      mindinsight/ui/src/locales/en-us.json
  7. +12
    -5
      mindinsight/ui/src/locales/zh-cn.json
  8. +188
    -247
      mindinsight/ui/src/views/explain/saliency-map.vue
  9. +296
    -133
      mindinsight/ui/src/views/explain/xai-metric.vue

mindinsight/ui/src/components/benchmarkBarChart.vue → mindinsight/ui/src/components/benchmark-bar-chart.vue View File

@@ -147,7 +147,7 @@ export default {
}, },
grid: { grid: {
left: '10%', left: '10%',
right: 260,
right: 280,
bottom: '3%', bottom: '3%',
top: 30, top: 30,
}, },

+ 325
- 0
mindinsight/ui/src/components/radar-chart.vue View File

@@ -0,0 +1,325 @@
<!--
Copyright 2020 Huawei Technologies Co., Ltd.All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<template>
<div class="radar"
ref="radar">
</div>
</template>
<script>
import echarts from 'echarts';
import common from '../common/common-property';

export default {
name: 'RadarChart',
data() {
return {
option: {}, // The chart option
instance: null, // The chart instance created by echarts init
indicators: [], // The list of indicator in string
defaultRadius: '73%', // The dafault radius of radar
defaultSplit: 4, // The dafault split length of indicator
defaultEWidth: 5, // The default width of emphasis width
defaultLegendSetting: {
padding: [0, 16],
itemWidth: 25,
itemHeight: 4,
textStyle: {
padding: [0, 0, 0, 4],
},
}, // The default setting of legend
};
},
props: [
'data', // The processed radar data
'nowHoverName', // The hover item name
'radius', // The radius of radar
'split', // The split length of indicator
'eWidth', // The width of emphasis width
'legendSetting', // The setting of legend
'ifTwo', // If show two legend item per line, default is 'true'
'ifResetTooltip', // If fix the tooltip in the upper left and right corner, default is 'true'
'resize', // If table need resize
],
mounted() {
this.initRadarChart(this.data);
if (typeof this.resize !== 'undefined' && this.resize) {
window.onresize = () => {
this.initRadarChart(this.data);
};
}
},
watch: {
/**
* The logic executed when hover item changed
*/
nowHoverName() {
this.emphasisRadarChart(this.nowHoverName);
},
},
methods: {
/**
* The logic of init radar chart with default setting
* @param {Object} data Original data
*/
initRadarChart(data) {
for (let i = 0; i < data.indicator.length; i++) {
this.indicators.push(data.indicator[i].name);
}
const dom = this.$refs.radar;
if (dom) {
this.instance = echarts.init(dom);
} else {
return;
}
this.instance.setOption({
tooltip: {
position: (pos, params, dom, rect, size) => {
if (
typeof this.ifResetTooltip !== 'undefined'
? this.ifResetTooltip
: true
) {
return pos[0] > size.viewSize[0] / 2
? {left: 0, top: 0}
: {right: 0, top: 0};
} else {
return null;
}
},
formatter: (params) => {
let temp = `${params.data.name}<br>`;
for (let i = 0; i < this.indicators.length; i++) {
temp += `${this.indicators[i]}: ${params.data.value[i]}<br>`;
}
return temp;
},
},
title: {
text: data.title ? data.title : '',
textStyle: {
lineHeight: '20',
fontSize: '14',
fontWeight: '600',
},
padding: [15, 16],
},
color: common.pieColorArr,
center: ['50%', '50%'],
radar: {
shape: 'circle',
name: {
textStyle: {
color: '#909399',
},
formatter: (text) => {
return this.formatIndicator(
text,
this.indicators,
this.split ? this.split : this.defaultSplit,
);
},
},
radius: this.radius ? this.radius : this.defaultRadius,
},
series: [
{
type: 'radar',
emphasis: {
lineStyle: {
width: this.eWidth ? this.eWidth : this.defaultEWidth,
},
},
data: [],
},
],
});
this.updateRadarChart(data);
},
/**
* The logic of update radar chart with new data
* The new data should be like that
* {legend: Array<string>, indicator: [{name: string,max: number}], series: [{value: [], name: string}]}
* @param {Object} data Original data
*/
updateRadarChart(data) {
this.instance.setOption({
legend: data.legend
? this.formatLegend(
data.legend,
this.legendSetting
? this.legendSetting
: this.defaultLegendSetting,
this.ifTwo ? this.ifTwo : true,
)
: [],
radar: {
indicator: data.indicator ? data.indicator : [],
},
series: [
{
data: data.series ? data.series : [],
},
],
});
},
/**
* The logic of update radar chart with new data
* The new data should be like that
* {legend: Array<string>, indicator: [{name: string,max: number}], series: [{value: [], name: string}]}
* @param {Object} seriesName The name of series item which need to be emphasised
*/
emphasisRadarChart(seriesName) {
const option = this.instance.getOption();
const series = Array.from(option.series[0].data);
for (let i = 0; i < series.length; i++) {
if (series[i].name === seriesName) {
series[i].lineStyle = {
width: this.eWidth ? this.eWidth : this.defaultEWidth,
};
} else {
// Line style rollback
Reflect.deleteProperty(series[i], 'lineStyle');
}
}
this.instance.setOption({
series: [
{
data: series,
},
],
});
},
/**
* The logic of init the legend style
* @param {Array<string>} legend Original data
* @param {Object} setting
* @param {boolean} two If make the legend layout with two item per line
* @return {Array<Object>}
*/
formatLegend(legend, setting, two) {
const newLegend = [];
let count = 0;
// The height of title and legend line
const titleHeigth = 50;
const legendHeight = 20;
for (let i = 0; i < legend.length; i++) {
if (two) {
let left = '';
let top = '';
if (i % 2 === 0) {
left = '0%';
top = `${count * legendHeight + titleHeigth}px`;
} else {
left = '50%';
top = `${count * legendHeight + titleHeigth}px`;
count++;
}
newLegend.push(
Object.assign(
{
data: [legend[i]],
top: top,
left: left,
},
setting,
),
);
} else {
return Object.assign(
{
data: legend,
top: '50px',
},
setting,
);
}
}
return newLegend;
},
/**
* The logic of resolve the problem that the indicator cant show complete
* @param {String} indicator
* @param {Array<string>} indicators
* @return {String}
*/
formatIndicator(indicator, indicators) {
if (!Array.isArray(indicators)) {
return indicator;
}
const index = indicators.indexOf(indicator);
if (index < 0) {
return indicator;
}
// 360 : Degree of circle
const degree = (360 / indicators.length) * index;
const radiusString = this.radius ? this.radius : this.defaultRadius;
// 100 : The parameter to convert percentage to decimal
const radiusNumber = radiusString.replace('%', '') / 100;
const dom = this.$refs.radar;
const indicatorSpace = this.getSpace(
degree,
radiusNumber,
dom.offsetWidth,
);
// 10 : The maximum PX of a single English letter in the current font size
const split = Math.ceil(indicatorSpace / 10);
const chars = indicator.split('');
for (let i = 0; i < chars.length; i++) {
if ((i + 1) % split === 0 && i !== chars.length - 1) {
chars[i] += '-\n';
}
}
return chars.join('');
},
/**
* The logic of cal the space of indicator
* @param {Number} degree
* @param {Number} radius
* @param {Number} width
* @return {Number}
*/
getSpace(degree, radius, width) {
const uprightDegree = 90; // Angle of quarter circle
if (degree === 0 || degree === uprightDegree * 2) {
return width;
}
if (degree === uprightDegree || degree === uprightDegree * 3) {
return (width - width * radius) / 2;
}
const x = Math.PI / (uprightDegree * 2);
const half = (width * radius) / 2;
let calDegree;
if (degree < uprightDegree) {
calDegree = degree;
} else if (uprightDegree < degree && degree < uprightDegree * 2) {
calDegree = uprightDegree - (degree % uprightDegree);
} else if (uprightDegree * 2 < degree && degree < uprightDegree * 3) {
calDegree = degree % uprightDegree;
} else {
calDegree = uprightDegree - (degree % uprightDegree);
}
const length = half * Math.sin(calDegree * x);
return width / 2 - length;
},
},
};
</script>
<style lang="scss">
.radar {
width: 100%;
height: 100%;
}
</style>

+ 319
- 0
mindinsight/ui/src/components/search-select.vue View File

@@ -0,0 +1,319 @@
<template>
<el-select v-model="selectedLabels"
:placeholder="$t('public.select')"
:multiple="typeof multiple !== 'undefined' ? multiple : false"
:collapse-tags="typeof collapseTags !== 'undefined' ? collapseTags : false"
class="cl-search-select">
<div class="cl-search-select-action">
<el-input v-model="filter"
:placeholder="$t('public.enter')"
id="search-select-valid"
class="action-gap"
clearable></el-input>
<span class="action-gap"
:class="{'able': functionAble, 'disable': !functionAble}"
@click="selectAll"
v-if="multiple">{{$t('public.selectAll')}}</span>
<span @click="clearAll"
:class="{'able': functionAble, 'disable': !functionAble}"
v-if="multiple">
{{$t('public.clear')}}</span>
</div>
<slot v-if="!ifReady" name="oriData"></slot>
<!-- Option -->
<template v-if="type === 'option'">
<el-option v-for="option of options"
:key="option.label"
:label="option.label"
:value="option.value"
:disabled="typeof option.disabled !== 'undefined' ? option.disabled : false">
</el-option>
</template>
<!-- Group -->
<template v-else>
<el-option-group v-for="group in groups"
:key="group.label"
:label="group.label"
:disabled="typeof group.disabled !== 'undefined' ? group.disabled : false">
<el-option v-for="option in group.options"
:key="option.value"
:label="option.label"
:value="option.value"
:disabled="typeof option.disabled !== 'undefined' ? option.disabled : false">
</el-option>
</el-option-group>
</template>
<div slot="empty">
<div class="cl-search-select-action cl-search-select-empty">
<el-input v-model="filter"
:placeholder="$t('public.enter')"
id="search-select-empty"
class="action-gap"
clearable></el-input>
<span class="action-gap"
:class="{'able': functionAble, 'disable': !functionAble}"
@click="selectAll"
v-if="multiple">{{$t('public.selectAll')}}</span>
<span @click="clearAll"
:class="{'able': functionAble, 'disable': !functionAble}"
v-if="multiple">
{{$t('public.clear')}}</span>
</div>
<div class="cl-search-select-nodata">{{$t('public.emptyData')}}</div>
</div>
</el-select>
</template>

<script>
export default {
props: {
type: String, // 'option' | 'group'
collapseTags: Boolean, // If open the collapse tags
multiple: Boolean, // If open the multiple
slotReady: Boolean, // If the slot loading is asynchronous, should let the component know the state of slot
},
data() {
return {
includes: undefined, // If the String.prototype.includes() is useful
ifReady: false, // If the option is ready
selectedLabels: undefined, // The selected labels
filter: '', // The value to filter the option
functionAble: false, // If the selectAll and clearAll button accessible
optionsTemp: [], // The options template includes all options
groupsTemp: [], // The groups template includes all groups and options
options: [], // The source of el-option that actually displayed
groups: [], // The source of el-option-group that actually displayed
};
},
watch: {
/**
* The logic to init the options when slot is ready
* @param {Boolean} val
*/
slotReady(val) {
if (val) {
this.$nextTick(() => {
if (this.$slots.oriData) {
this.init(this.type, this.$slots.oriData);
} else {
throw new Error('Wrong time');
}
});
}
},
/**
* The logic to filter options
* @param {String} val The input word
*/
filter(val) {
if (val !== '') {
this.functionAble = false;
} else {
this.functionAble = true;
}
if (this.type === 'option') {
this.options = this.optionsTemp.filter((option) => {
if (this.includes) {
return option.label.includes(val);
} else {
return option.label.indexOf(val) >= 0;
}
});
} else {
for (let i = 0; i < this.groupsTemp.length; i++) {
this.groups[i] = Object.assign({}, this.groupsTemp[i]);
this.groups[i].options = this.groupsTemp[i].options.filter(
(option) => {
if (this.includes) {
return option.label.includes(val);
} else {
return option.label.indexOf(val) >= 0;
}
},
);
}
this.groups = this.groups.filter((val) => {
return val.options.length !== 0;
});
}
this.$nextTick(() => {
if (this.type === 'option') {
this.refocus(this.options.length);
} else {
this.refocus(this.groups.length);
}
});
},
/**
* The logic executed when selected labels changed
* @param {String} val The input word
*/
selectedLabels(val) {
this.$emit('selectedUpdate', val);
},
},
methods: {
/**
* The logic of click selectAll
*/
selectAll() {
if (!this.functionAble) {
return;
}
this.selectedLabels = [];
if (this.type === 'option') {
this.optionsTemp.forEach((element) => {
this.selectedLabels.push(element.value);
});
} else {
for (let i = 0; i < this.groupsTemp.length; i++) {
this.groupsTemp[i].options.forEach((element) => {
this.selectedLabels.push(element.value);
});
}
}
},
/**
* The logic of click clear
*/
clearAll() {
if (!this.functionAble) {
return;
}
this.selectedLabels = [];
},
/**
* The logic of init options by slot data when type is opitons
* @param {Array<Object>} vnodes
*/
initOptions(vnodes) {
this.optionsTemp = [];
for (let i = 0; i < vnodes.length; i++) {
this.optionsTemp[i] = Object.assign(
{},
vnodes[i].componentOptions.propsData,
);
}
this.options = Array.from(this.optionsTemp);
this.functionAble = true;
this.ifReady = true;
},
/**
* The logic of init options by slot data when type is groups
* @param {Array<Object>} vnodes
*/
initGroups(vnodes) {
this.groupsTemp = [];
for (let i = 0; i < vnodes.length; i++) {
this.groupsTemp[i] = Object.assign(
{},
vnodes[i].componentOptions.propsData,
);
this.groupsTemp[i].options = [];
for (let j = 0; j < vnodes[i].componentOptions.children.length; j++) {
this.groupsTemp[i].options[j] = Object.assign(
{},
vnodes[i].componentOptions.children[j].componentOptions.propsData,
);
}
}
this.groups = Array.from(this.groupsTemp);
this.functionAble = true;
this.ifReady = true;
},
/**
* The logic of init options by slot data when type is groups
* @param {number} length The filter word length
*/
refocus(length) {
if (length === 0) {
const dom = document.getElementById('search-select-empty');
if (dom) {
dom.focus();
}
} else {
const dom = document.getElementById('search-select-valid');
if (dom) {
dom.focus();
}
}
},
/**
* The logic of init options by slot data
* @param {String} type The type
* @param {Array<Object>} slots
*/
init(type, slots) {
if (type === 'option') {
this.initOptions(slots);
} else if (type === 'group') {
this.initGroups(slots);
} else {
throw new Error(
`Wrong type. The value of type can only be one of 'option' or 'group'`,
);
}
},
},
created() {
if (this.multiple) {
this.selectedLabels = '';
} else {
this.selectedLabels = [];
}
if (typeof String.prototype.includes !== 'function') {
this.includes = false;
} else {
this.includes = true;
}
},
mounted() {
if (typeof this.slotReady === 'undefined') {
if (this.$slots.oriData) {
this.init(this.type, this.$slots.oriData);
} else {
throw new Error('Slot is not ready.');
}
}
},
};
</script>

<style lang="scss">
.cl-search-select {
width: 100%;
}
.cl-search-select-action {
padding-right: 10px;
padding-left: 10px;
display: flex;
align-items: center;
.el-input {
width: 0;
flex-grow: 1;
.el-input__inner {
padding: 0 9px;
}
}
.action-gap {
margin-right: 6px;
}
.able {
color: #00a5a7;
cursor: pointer;
}
.disable {
color: #c3c3c3;
cursor: not-allowed;
}
}
.cl-search-select-empty {
padding-top: 6px;
}
.cl-search-select-nodata {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
}
</style>

mindinsight/ui/src/components/selectGroup.vue → mindinsight/ui/src/components/select-group.vue View File

@@ -148,5 +148,9 @@ export default {
margin-right: 20px; margin-right: 20px;
height: 32px; height: 32px;
} }
.checkboxes-last {
display: flex;
align-items: center;
}
} }
</style> </style>

mindinsight/ui/src/components/superposeImg.vue → mindinsight/ui/src/components/superpose-img.vue View File


+ 12
- 5
mindinsight/ui/src/locales/en-us.json View File

@@ -443,6 +443,9 @@
"minddata_get_next_queue": { "minddata_get_next_queue": {
"desc": "The ratio of empty data queues is {n1}/{n2}." "desc": "The ratio of empty data queues is {n1}/{n2}."
}, },
"minddata_device_queue": {
"desc": "The ratio of empty queues on a host is {n1}/{n2}, and the ratio of full queues is {n3}/{n4}."
},
"dataProcess": "This shows the data processing. Data is stored in the host queue during data processing, and then stored in the data queue during data transmission. Finally, the forward and backward propagation get_next transmits the data to forward propagation.", "dataProcess": "This shows the data processing. Data is stored in the host queue during data processing, and then stored in the data queue during data transmission. Finally, the forward and backward propagation get_next transmits the data to forward propagation.",
"dataProcessInfo": "By determining the empty host and data queues, you can preliminarily determine the stage where the performance is abnormal.", "dataProcessInfo": "By determining the empty host and data queues, you can preliminarily determine the stage where the performance is abnormal.",
"analysisOne": "1. If the step interval is long and some batches of the data queue are empty, the performance is abnormal during data processing and transmission. Otherwise, locate the internal problem of the forward and backward propagation get_next.", "analysisOne": "1. If the step interval is long and some batches of the data queue are empty, the performance is abnormal during data processing and transmission. Otherwise, locate the internal problem of the forward and backward propagation get_next.",
@@ -709,25 +712,29 @@
"viewScore": "View Score", "viewScore": "View Score",
"fetch": "Filter", "fetch": "Filter",
"minConfidence": "Probability Threshold", "minConfidence": "Probability Threshold",
"confidenceRange": "Probability and Range",
"imgSort": "Sort Images By", "imgSort": "Sort Images By",
"default": "Default", "default": "Default",
"byProbability": "Probabilities in descending order", "byProbability": "Probabilities in descending order",
"byUncertainty": "Uncertainties in descending order",
"uncertainty": "uncertainty",
"superposeImg": "Overlay on Original Image", "superposeImg": "Overlay on Original Image",
"originalPicture": "Original Image", "originalPicture": "Original Image",
"forecastTag": "Prediction Tag", "forecastTag": "Prediction Tag",
"tag": "Tag", "tag": "Tag",
"confidence": "Probability", "confidence": "Probability",
"forecastTagTip": "When the inference image has the correct tag, the following four flags are displayed in the tag row", "forecastTagTip": "When the inference image has the correct tag, the following four flags are displayed in the tag row",
"TP": "TP: indicates true positive. The tag is a positive sample, and the classification is a positive sample;",
"FN": "FN: indicates false negative. The tag is a positive sample, and the classification is a negative sample;",
"FP": "FP: indicates false positive. The tag is a negative sample, and the classification is a positive sample;",
"TN": "TN: indicates true negative. The tag is a negative sample, and the classification is a negative sample;",
"TP": "TP, indicates true positive. The tag is a positive sample, and the classification is a positive sample;",
"FN": "FN, indicates false negative. The tag is a positive sample, and the classification is a negative sample;",
"FP": "FP, indicates false positive. The tag is a negative sample, and the classification is a positive sample;",
"TN": "TN, Indicates true negative. The tag is a negative sample, and the classification is a negative sample;",
"mainTipTitle": "Function description:", "mainTipTitle": "Function description:",
"mainTipPartOne": "This function visualizes the basis for model classification. After the to-be-explained model, the image, and the tag are selected, a contribution degree of each pixel in the original image to the selected tag is calculated by using an explanation method, and visualization is performed by using a saliency map similar to a heatmap. A brighter color indicates that the corresponding area contributes more to the selected tag of the model prediction. The darker the color, the smaller the contribution of the area to the selected tag.", "mainTipPartOne": "This function visualizes the basis for model classification. After the to-be-explained model, the image, and the tag are selected, a contribution degree of each pixel in the original image to the selected tag is calculated by using an explanation method, and visualization is performed by using a saliency map similar to a heatmap. A brighter color indicates that the corresponding area contributes more to the selected tag of the model prediction. The darker the color, the smaller the contribution of the area to the selected tag.",
"mainTipPartTwo": "A saliency map helps you understand the features related to the specified tag during deep neural network inference. When the inference basis and expectation of a model are different, you can debug the model by referring to the saliency map so that the model can perform inference based on proper features.", "mainTipPartTwo": "A saliency map helps you understand the features related to the specified tag during deep neural network inference. When the inference basis and expectation of a model are different, you can debug the model by referring to the saliency map so that the model can perform inference based on proper features.",
"mainTipPartThree": "For details about how to generate saliency maps, see section 3.2 'Local Methods' in", "mainTipPartThree": "For details about how to generate saliency maps, see section 3.2 'Local Methods' in",
"mainTipPartFour": "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9050829", "mainTipPartFour": "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9050829",
"noExplainer": "Select Explanation Method"
"noExplainer": "Select Explanation Method",
"minConfidenceTip": "Probability threshold of a prediction tag. A tag is recorded as a prediction tag if its output probability is greater than the threshold. "
}, },
"metric": { "metric": {
"scoreSystem": "Scoring System", "scoreSystem": "Scoring System",


+ 12
- 5
mindinsight/ui/src/locales/zh-cn.json View File

@@ -442,6 +442,9 @@
"minddata_get_next_queue": { "minddata_get_next_queue": {
"desc": "数据队列为空比例{n1}/{n2}。" "desc": "数据队列为空比例{n1}/{n2}。"
}, },
"minddata_device_queue": {
"desc": "主机侧队列为空比例{n1}/{n2},为满比例{n3}/{n4}。"
},
"dataProcess": "该图展示了数据处理阶段的流程,数据通过数据处理阶段存入主机队列,再通过数据传输阶段存入数据队列,最终由数据传输算子get_next发送给前向训练使用。", "dataProcess": "该图展示了数据处理阶段的流程,数据通过数据处理阶段存入主机队列,再通过数据传输阶段存入数据队列,最终由数据传输算子get_next发送给前向训练使用。",
"dataProcessInfo": "综合分析该阶段的流程,通过判断主机队列和数据队列为空的情况就可以初步判断可能出现性能异常的阶段。", "dataProcessInfo": "综合分析该阶段的流程,通过判断主机队列和数据队列为空的情况就可以初步判断可能出现性能异常的阶段。",
"analysisOne": "1、如果迭代间隙较长,并且数据队列部分batch为空,那么可能由于数据处理和数据传输阶段导致的性能异常,参考2,反之则定位数据传输算子get_next内部问题;", "analysisOne": "1、如果迭代间隙较长,并且数据队列部分batch为空,那么可能由于数据处理和数据传输阶段导致的性能异常,参考2,反之则定位数据传输算子get_next内部问题;",
@@ -706,25 +709,29 @@
"viewScore": "查看评分", "viewScore": "查看评分",
"fetch": "筛选", "fetch": "筛选",
"minConfidence": "概率阈值", "minConfidence": "概率阈值",
"confidenceRange": "概率(区间)",
"imgSort": "图片排序", "imgSort": "图片排序",
"default": "默认", "default": "默认",
"byProbability": "概率值降序", "byProbability": "概率值降序",
"byUncertainty": "不确定性值降序",
"uncertainty": "不确定性",
"superposeImg": "叠加于原图", "superposeImg": "叠加于原图",
"originalPicture": "原始图片", "originalPicture": "原始图片",
"forecastTag": "预测标签", "forecastTag": "预测标签",
"tag": "标签", "tag": "标签",
"confidence": "概率", "confidence": "概率",
"forecastTagTip": "当推理图片带有正确标签时,标签行会显示下列四种旗标", "forecastTagTip": "当推理图片带有正确标签时,标签行会显示下列四种旗标",
"TP": "TP代表Ture Positive,标签为正样本,分类为正样本;",
"FN": "FN代表False Negative,标签为正样本,分类为负样本;",
"FP": "FP代表Fasle Positive,标签为负样本,分类为正样本;",
"TN": "TN代表Ture Negative,标签为负样本,分类为负样本;",
"TP": "TP代表Ture Positive,标签为正样本,分类为正样本;",
"FN": "FN代表False Negative,标签为正样本,分类为负样本;",
"FP": "FP代表Fasle Positive,标签为负样本,分类为正样本;",
"TN": "TN代表Ture Negative,标签为负样本,分类为负样本;",
"mainTipTitle": "功能说明:", "mainTipTitle": "功能说明:",
"mainTipPartOne": "本功能对模型分类的依据进行可视化。选定待解释模型、图片和标签后,解释方法计算得到原始图像中每个像素对选定标签的贡献度,以类似热力图的显著图进行可视。显著图颜色越亮,表示对应区域对于模型预测选定标签的贡献越多;颜色越暗,该区域对选定标签的贡献越小。", "mainTipPartOne": "本功能对模型分类的依据进行可视化。选定待解释模型、图片和标签后,解释方法计算得到原始图像中每个像素对选定标签的贡献度,以类似热力图的显著图进行可视。显著图颜色越亮,表示对应区域对于模型预测选定标签的贡献越多;颜色越暗,该区域对选定标签的贡献越小。",
"mainTipPartTwo": "显著图可以帮助我们了解深度神经网络推理时,和指定标签有关系的特征。当模型推理依据和期望不同时,可以参考显著图对模型进行调试,让模型依据合理的特征进行推理。", "mainTipPartTwo": "显著图可以帮助我们了解深度神经网络推理时,和指定标签有关系的特征。当模型推理依据和期望不同时,可以参考显著图对模型进行调试,让模型依据合理的特征进行推理。",
"mainTipPartThree": "主流生成显著图的解释方法可以参考论文3.2节Local Methods:", "mainTipPartThree": "主流生成显著图的解释方法可以参考论文3.2节Local Methods:",
"mainTipPartFour": "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9050829", "mainTipPartFour": "https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9050829",
"noExplainer": "请选择解释方法"
"noExplainer": "请选择解释方法",
"minConfidenceTip": "预测标签的概率阈值,当一个标签的输出概率大于阈值时就会被记为预测标签。"
}, },
"metric": { "metric": {
"scoreSystem": "评分体系", "scoreSystem": "评分体系",


+ 188
- 247
mindinsight/ui/src/views/explain/saliency-map.vue View File

@@ -47,14 +47,12 @@ limitations under the License.
<!-- Explanation Method --> <!-- Explanation Method -->
<div class="cl-saliency-map-methods"> <div class="cl-saliency-map-methods">
<!-- Explainer Checkbox --> <!-- Explainer Checkbox -->
<div class="methods-right">
<SelectGroup :checkboxes="allExplainers"
@updateCheckedList="updateSelectedExplainers"
:title="$t('explain.explainMethod')">
<span class="methods-action"
@click="goMetric">{{$t('explain.viewScore')}}</span>
</SelectGroup>
</div>
<select-group :checkboxes="allExplainers"
@updateCheckedList="updateSelectedExplainers"
:title="$t('explain.explainMethod')">
<span class="methods-action"
@click="goMetric">{{$t('explain.viewScore')}}</span>
</select-group>
</div> </div>
<!-- Parameters Fetch --> <!-- Parameters Fetch -->
<div class="cl-saliency-map-condition"> <div class="cl-saliency-map-condition">
@@ -62,16 +60,17 @@ limitations under the License.
<div class="condition-item line-title">{{ $t('explain.tag') }}</div> <div class="condition-item line-title">{{ $t('explain.tag') }}</div>
<!-- Truth Labels --> <!-- Truth Labels -->
<div class="condition-item"> <div class="condition-item">
<el-select v-model="selectedTruthLabels"
:placeholder="$t('public.select')"
multiple
filterable
collapse-tags>
<search-select type="option"
:multiple="true"
:collapseTags="true"
@selectedUpdate="updateSelected"
:slotReady="labelReady">
<el-option v-for="label in truthLabels" <el-option v-for="label in truthLabels"
:key="label" :key="label"
:label="label" :label="label"
:value="label"></el-option>
</el-select>
:value="label"
slot="oriData"></el-option>
</search-select>
</div> </div>
<!-- Button --> <!-- Button -->
<div class="condition-item"> <div class="condition-item">
@@ -79,8 +78,21 @@ limitations under the License.
class="condition-button" class="condition-button"
@click="fetch">{{ $t('explain.fetch') }}</el-button> @click="fetch">{{ $t('explain.fetch') }}</el-button>
</div> </div>
<!-- Min Confidence -->
<div class="condition-item"> <div class="condition-item">
{{ $t('explain.minConfidence') + $t('symbols.colon')}}{{minConfidence}}</div>
{{ $t('explain.minConfidence')}}
<el-tooltip placement="bottom-start"
effect="light"
popper-class="confidence-tooltip">
<div slot="content"
class="tooltip-container">
{{$t('explain.minConfidenceTip')}}
</div>
<i class="el-icon-info"></i>
</el-tooltip>
{{$t('symbols.colon')}}
{{minConfidence}}
</div>
</div> </div>
<div class="condition-right"> <div class="condition-right">
<!-- Sorted Name --> <!-- Sorted Name -->
@@ -124,7 +136,7 @@ limitations under the License.
v-else> v-else>
<el-table :data="tableData" <el-table :data="tableData"
border border
:height="tableHeight"
height="100%"
:span-method="mergeTable"> :span-method="mergeTable">
<!-- Original Picture Column--> <!-- Original Picture Column-->
<el-table-column :label="$t('explain.originalPicture')" <el-table-column :label="$t('explain.originalPicture')"
@@ -154,18 +166,28 @@ limitations under the License.
</div> </div>
<div class="tip-item"> <div class="tip-item">
<img :src="require('@/assets/images/explain-tp.svg')" <img :src="require('@/assets/images/explain-tp.svg')"
alt="">
class="tip-icon">
{{$t('symbols.colon')}}
{{$t('explain.TP')}}
</div>
<div class="tip-item">
<img :src="require('@/assets/images/explain-fn.svg')" <img :src="require('@/assets/images/explain-fn.svg')"
alt="">
class="tip-icon">
{{$t('symbols.colon')}}
{{$t('explain.FN')}}
</div>
<div class="tip-item">
<img :src="require('@/assets/images/explain-fp.svg')" <img :src="require('@/assets/images/explain-fp.svg')"
alt="">
class="tip-icon">
{{$t('symbols.colon')}}
{{$t('explain.FP')}}
</div>
<div class="tip-item">
<img :src="require('@/assets/images/explain-tn.svg')" <img :src="require('@/assets/images/explain-tn.svg')"
alt="">
class="tip-icon">
{{$t('symbols.colon')}}
{{$t('explain.TN')}}
</div> </div>
<div class="tip-item">{{$t('explain.TP')}}</div>
<div class="tip-item">{{$t('explain.FN')}}</div>
<div class="tip-item">{{$t('explain.FP')}}</div>
<div class="tip-item">{{$t('explain.TN')}}</div>
</div> </div>
</div> </div>
<i class="el-icon-info"></i> <i class="el-icon-info"></i>
@@ -173,7 +195,43 @@ limitations under the License.
</span> </span>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
<div class="table-forecast-tag">
<div class="table-forecast-tag"
v-if="uncertaintyEnabled">
<!-- Tag Title -->
<div class="tag-title-true">
<div class="first">{{ $t('explain.tag') }}</div>
<div>{{ $t('explain.confidenceRange') }}</div>
<div class="center">{{ $t('explain.uncertainty') }}</div>
</div>
<!-- Tag content -->
<div class="tag-content">
<div v-for="(tag, index) in scope.row.inferences"
:key="tag.label"
class="tag-content-item tag-content-item-true"
:class="{
'tag-active': index === scope.row.activeLabelIndex,
'tag-tp': tag.type === 'tp',
'tag-fn': tag.type === 'fn',
'tag-fp': tag.type === 'fp'
}"
@click="changeActiveLabel(scope.row, index)">
<div class="first">{{ tag.label }}</div>
<div>
<div>{{ tag.confidence.toFixed(3) }}</div>
<div>{{
Math.floor(tag.confidence_itl95[0] * 100) / 100 +
'-' +
Math.ceil(tag.confidence_itl95[1] * 100) / 100
}}</div>
</div>
<div class="center">
{{ tag.confidence_sd.toFixed(2) }}
</div>
</div>
</div>
</div>
<div class="table-forecast-tag"
v-else>
<!--Tag Title--> <!--Tag Title-->
<div class="tag-title-false"> <div class="tag-title-false">
<div></div> <div></div>
@@ -186,10 +244,10 @@ limitations under the License.
:key="tag.label" :key="tag.label"
class="tag-content-item tag-content-item-false" class="tag-content-item tag-content-item-false"
:class="{ :class="{
'tag-active': index == scope.row.activeLabelIndex,
'tag-tp': tag.type == 'tp',
'tag-fn': tag.type == 'fn',
'tag-fp': tag.type == 'fp'
'tag-active': index === scope.row.activeLabelIndex,
'tag-tp': tag.type === 'tp',
'tag-fn': tag.type === 'fn',
'tag-fp': tag.type === 'fp'
}" }"
@click="changeActiveLabel(scope.row, index)"> @click="changeActiveLabel(scope.row, index)">
<div></div> <div></div>
@@ -210,17 +268,17 @@ limitations under the License.
<span :title="explainer">{{explainer}}</span> <span :title="explainer">{{explainer}}</span>
</template> </template>
<template slot-scope="scope"> <template slot-scope="scope">
<SuperpostImgComponent v-if="scope.row.inferences[scope.row.activeLabelIndex][explainer]"
containerSize="250"
:backgroundImg="getImgURL(scope.row.image)"
:targetImg="getImgURL(scope.row.inferences[scope.row.activeLabelIndex][explainer])"
:ifSuperpose="ifSuperpose"
@click.native="showImgDiglog(scope.row, explainer)">
</SuperpostImgComponent>
<superpose-img v-if="scope.row.inferences[scope.row.activeLabelIndex][explainer]"
containerSize="250"
:backgroundImg="getImgURL(scope.row.image)"
:targetImg="getImgURL(scope.row.inferences[scope.row.activeLabelIndex][explainer])"
:ifSuperpose="ifSuperpose"
@click.native="showImgDiglog(scope.row, explainer)">
</superpose-img>
</template> </template>
</el-table-column> </el-table-column>
<!-- None selected explainer Column--> <!-- None selected explainer Column-->
<el-table-column v-if="selectedExplainers.length == 0"
<el-table-column v-if="selectedExplainers.length === 0"
label="" label=""
class-name="no-method-cell"> class-name="no-method-cell">
<template> <template>
@@ -247,92 +305,47 @@ limitations under the License.
:total="pageInfo.total" :total="pageInfo.total"
v-show="!ifError"></el-pagination> v-show="!ifError"></el-pagination>
</div> </div>
<!-- The dialog of nine similar pictures -->
<el-dialog :title="$t('explain.ninePictures')"
:visible.sync="similarDialog.visible"
v-if="similarDialog.visible"
top="50px"
width="660px"
@close="dialogClose">
<div class="cl-saliency-map-dialog">
<div class="dialog-text">
<div class="dot"></div>
<div class="text">{{ $t('explain.theMeaningOfBorderColor') }}</div>
</div>
<div class="dialog-text">
<div class="dot"></div>
<div class="text">{{ $t('explain.theMeaningOfEightPicture') }}</div>
</div>
<div class="dialog-label">
{{$t('explain.tagAndLegend') + $t('symbols.colon')}}
<span class="label-item">{{ similarDialog.label }}</span>
</div>
<div class="dialog-grid-container">
<div class="dialog-grid"
v-if="!similarDialog.loading">
<div class="grid-item"
v-for="dialogItem in similarDialog.around"
:key="dialogItem.image"
:style="{'backgroundColor': dialogItem.color}">
<img :src="getImgURL(dialogItem.image)"
alt="">
</div>
<div class="grid-item grid-center">
<img :src="getImgURL(similarDialog.center.image)"
alt="">
</div>
</div>
<div class="dialog-grid"
v-else
v-loading="true">
<div class="grid-item"
v-for="index in 9"
:key="index">
</div>
</div>
</div>
</div>

</el-dialog>
<!-- Show Img Dialog -->
<el-dialog :title="imageDetails.title" <el-dialog :title="imageDetails.title"
:visible.sync="imageDetails.imgShow" :visible.sync="imageDetails.imgShow"
v-if="imageDetails.imgShow" v-if="imageDetails.imgShow"
top="100px" top="100px"
width="560px"> width="560px">
<div class="detail-container"> <div class="detail-container">
<SuperpostImgComponent containerSize="500"
:backgroundImg="imageDetails.imgUrl"
:targetImg="imageDetails.targetUrl"
:ifSuperpose="ifSuperpose">
</SuperpostImgComponent>
<superpose-img containerSize="500"
:backgroundImg="imageDetails.imgUrl"
:targetImg="imageDetails.targetUrl"
:ifSuperpose="ifSuperpose">
</superpose-img>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>


<script> <script>
import SelectGroup from '../../components/selectGroup';
import SuperpostImgComponent from '../../components/superposeImg';
import selectGroup from '../../components/select-group';
import superposeImg from '../../components/superpose-img';
import searchSelect from '../../components/search-select';
import requestService from '../../services/request-service.js'; import requestService from '../../services/request-service.js';
import {basePath} from '@/services/fetcher'; import {basePath} from '@/services/fetcher';


export default { export default {
components: { components: {
SelectGroup,
SuperpostImgComponent,
selectGroup,
superposeImg,
searchSelect,
}, },
data() { data() {
return { return {
trainID: null, // The id of the train
trainID: '', // The id of the train
selectedExplainers: [], // The selected explainer methods selectedExplainers: [], // The selected explainer methods
allExplainers: [], // The list of all explainer method allExplainers: [], // The list of all explainer method
similarDialog: {
visible: false,
loading: true,
around: [], // The eight images around
center: null, // The center image
label: null,
}, // The object of similar dialog
imageDetails: {
title: '',
imgUrl: '',
targetUrl: '',
imgShow: false,
}, // The object of show img dialog
ifSuperpose: false, // If open the superpose function ifSuperpose: false, // If open the superpose function
ifTableLoading: true, // If the table waiting for the data ifTableLoading: true, // If the table waiting for the data
ifError: false, // If request error ifError: false, // If request error
@@ -340,6 +353,7 @@ export default {
tableData: null, // The table data tableData: null, // The table data
selectedTruthLabels: [], // The selected truth labels selectedTruthLabels: [], // The selected truth labels
truthLabels: [], // The list of all truth labels truthLabels: [], // The list of all truth labels
// truthLabelsTemp: [], // The list of all truth labels
sortedName: 'confidence', // The sorted Name of sort sortedName: 'confidence', // The sorted Name of sort
sortedNames: [ sortedNames: [
{ {
@@ -355,17 +369,8 @@ export default {
}, // The object of pagination information }, // The object of pagination information
uncertaintyEnabled: null, // If open the uncertainty api uncertaintyEnabled: null, // If open the uncertainty api
tableHeight: 0, // The height of table to fix the table header tableHeight: 0, // The height of table to fix the table header
imageDetails: {
title: '',
imgUrl: '',
targetUrl: '',
imgShow: false,
}, // The object of click canvas dialog
queryParameters: null, // The complete parameters of query table information, have pagination information queryParameters: null, // The complete parameters of query table information, have pagination information
ifCalHeight: {
mounted: false,
serviced: false,
}, // The Effectiveness of calculate the height of table, when the doms and the explainer checkboxs are ready
labelReady: false, // If the truth labels are ready
}; };
}, },
computed: { computed: {
@@ -380,8 +385,19 @@ export default {
}, },
}, },
methods: { methods: {
updateSelectedExplainers(newList) {
this.selectedExplainers = newList;
/**
* The logic of update selected explainers
* @param {Object} newVal The updated list
*/
updateSelectedExplainers(newVal) {
this.selectedExplainers = newVal;
},
/**
* The logic of update selected truth labels
* @param {Object} newVal The updated list
*/
updateSelected(newVal) {
this.selectedTruthLabels = newVal;
}, },
/** /**
* Get the complete url of image * Get the complete url of image
@@ -389,20 +405,8 @@ export default {
* @return {string} The complete url of image * @return {string} The complete url of image
*/ */
getImgURL(url) { getImgURL(url) {
return `${basePath}${url}&date=${new Date().getTime()}`;
},
/**
* The logic of click the explainer method canvas
* @param {Object} rowObj The object of table row in element-ui table
* @param {string} title The title of the dialog
*/
showImgDiglog(rowObj, title) {
this.imageDetails.title = title;
this.imageDetails.imgUrl = this.getImgURL(rowObj.image);
this.imageDetails.targetUrl = this.getImgURL(
rowObj.inferences[rowObj.activeLabelIndex][title],
);
this.imageDetails.imgShow = true;
const newURL = `${basePath}${url}&date=${new Date().getTime()}`;
return newURL.replace(/(?<!:)\/\//g, '/');
}, },
/** /**
* The logic of query page information by non-default parameters * The logic of query page information by non-default parameters
@@ -437,15 +441,24 @@ export default {
* @param {string} val The sorted name now * @param {string} val The sorted name now
*/ */
sortedNameChange(val) { sortedNameChange(val) {
if (val !== null) {
this.queryParameters.sorted_name = val;
} else {
Reflect.deleteProperty(this.queryParameters, 'sorted_name');
}
this.queryParameters.sorted_name = val;
this.pageInfo.currentPage = 1; this.pageInfo.currentPage = 1;
this.queryParameters.offset = this.pageInfo.currentPage - 1; this.queryParameters.offset = this.pageInfo.currentPage - 1;
this.queryPageInfo(this.queryParameters); this.queryPageInfo(this.queryParameters);
}, },
/**
* The logic of click the explainer method canvas
* @param {Object} rowObj The object of table row in element-ui table
* @param {string} title The title of the dialog
*/
showImgDiglog(rowObj, title) {
this.imageDetails.title = title;
this.imageDetails.imgUrl = this.getImgURL(rowObj.image);
this.imageDetails.targetUrl = this.getImgURL(
rowObj.inferences[rowObj.activeLabelIndex][title],
);
this.imageDetails.imgShow = true;
},
/** /**
* Request basic inforamtion of train * Request basic inforamtion of train
* @param {Object} params Parameters of the request basic inforamtion of train interface * @param {Object} params Parameters of the request basic inforamtion of train interface
@@ -472,23 +485,32 @@ export default {
truthLabels.push(res.data.classes[i].label); truthLabels.push(res.data.classes[i].label);
} }
this.truthLabels = truthLabels; this.truthLabels = truthLabels;
this.$nextTick(() => {
if (this.truthLabels.length !== 0) {
this.labelReady = true;
}
});
} }
if (res.data.uncertainty) { if (res.data.uncertainty) {
this.uncertaintyEnabled = res.data.uncertainty.enabled this.uncertaintyEnabled = res.data.uncertainty.enabled
? true ? true
: false; : false;
// The sort by uncertainty only valid when uncertaintyEnabled is true
if (this.uncertaintyEnabled) {
this.sortedNames.push({
label: this.$t('explain.byUncertainty'),
value: 'uncertainty',
});
}
} }
} }
this.ifCalHeight.serviced = true; // The explainer checkboxs are ready
resolve(true); resolve(true);
}, },
(error) => { (error) => {
this.ifCalHeight.serviced = true;
reject(error); reject(error);
}, },
) )
.catch((error) => { .catch((error) => {
this.ifCalHeight.serviced = true;
reject(error); reject(error);
}); });
}); });
@@ -662,22 +684,6 @@ export default {
return [1, 1]; return [1, 1];
} }
}, },
/**
* Calculate the height of table to let the table header fixed
*/
calTableHeight() {
const table = document.getElementsByClassName('cl-saliency-map-table')[0];
if (table !== undefined || table !== null) {
this.$nextTick(() => {
const height = table.clientHeight;
this.tableHeight = height - 21; // The table container padding-top
});
}
window.onresize = () => {
const height = table.clientHeight;
this.tableHeight = height - 21;
};
},
/** /**
* Go to the metric page * Go to the metric page
*/ */
@@ -688,17 +694,6 @@ export default {
}); });
}, },
}, },
watch: {
// The watcher of ifCalHeight to calculate the table height at the right time
ifCalHeight: {
handler() {
if (this.ifCalHeight.serviced && this.ifCalHeight.mounted) {
this.calTableHeight();
}
},
deep: true,
},
},
created() { created() {
if (!this.$route.query.id) { if (!this.$route.query.id) {
this.$message.error(this.$t('trainingDashboard.invalidId')); this.$message.error(this.$t('trainingDashboard.invalidId'));
@@ -726,7 +721,6 @@ export default {
}); });
}, },
mounted() { mounted() {
this.ifCalHeight.mounted = true; // The doms are ready
// Change the page title // Change the page title
if (this.$route.query.id) { if (this.$route.query.id) {
document.title = `${decodeURIComponent(this.$route.query.id)}-${this.$t( document.title = `${decodeURIComponent(this.$route.query.id)}-${this.$t(
@@ -806,6 +800,11 @@ export default {
font-size: 12px; font-size: 12px;
color: #575d6c; color: #575d6c;
white-space: nowrap; white-space: nowrap;
display: flex;
align-items: center;
.tip-icon {
margin-right: 4px;
}
} }
.tip-item:last-of-type { .tip-item:last-of-type {
margin-bottom: 0px; margin-bottom: 0px;
@@ -818,20 +817,8 @@ export default {
} }
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
$horizontalPadding: 32px;
$titleHeight: 56px;
$titlePadding: 0 $horizontalPadding;
$methodsPadding: 8px $horizontalPadding 12px $horizontalPadding;
$methodsLineHeight: 19px;
$conditionHeight: 58px;
$conditionPadding: 0px $horizontalPadding 21px $horizontalPadding;
$tablePadding: 21px $horizontalPadding 0 $horizontalPadding;
$paginationHeight: 60px;
$paginationPadding: 0 $horizontalPadding;
$tagFontSize: 12px;
.cl-saliency-map { .cl-saliency-map {
height: 100%; height: 100%;
width: 100%;
box-sizing: border-box; box-sizing: border-box;
background-color: #ffffff; background-color: #ffffff;
display: flex; display: flex;
@@ -839,8 +826,8 @@ $tagFontSize: 12px;
.cl-saliency-map-title { .cl-saliency-map-title {
display: flex; display: flex;
align-items: center; align-items: center;
height: $titleHeight;
padding: $titlePadding;
height: 56px;
padding: 0 32px;
font-size: 20px; font-size: 20px;
color: #282b33; color: #282b33;
letter-spacing: -0.86px; letter-spacing: -0.86px;
@@ -856,7 +843,7 @@ $tagFontSize: 12px;
margin-right: 0px !important; margin-right: 0px !important;
} }
.cl-saliency-map-methods { .cl-saliency-map-methods {
padding: $methodsPadding;
padding: 8px 32px 12px 32px;
.methods-action { .methods-action {
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
@@ -865,8 +852,8 @@ $tagFontSize: 12px;
} }
} }
.cl-saliency-map-condition { .cl-saliency-map-condition {
padding: $conditionPadding;
height: $conditionHeight;
padding: 0px 32px 21px 32px;
height: 58px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -885,6 +872,10 @@ $tagFontSize: 12px;
border-radius: 2px; border-radius: 2px;
border: 1px solid #00a5a7; border: 1px solid #00a5a7;
} }
.el-icon-info {
margin-right: 4px;
margin-left: 2px;
}
} }
} }
.condition-right { .condition-right {
@@ -904,12 +895,11 @@ $tagFontSize: 12px;
} }
} }
.cl-saliency-map-table { .cl-saliency-map-table {
padding: $tablePadding;
padding: 21px 32px 0 32px;
flex-grow: 1; flex-grow: 1;
overflow: hidden; overflow: hidden;
.table-nodata { .table-nodata {
height: 100%; height: 100%;
width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -921,10 +911,8 @@ $tagFontSize: 12px;
} }
.table-data { .table-data {
height: 100%; height: 100%;
width: 100%;
.table-forecast-tag { .table-forecast-tag {
height: 100%; height: 100%;
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.center { .center {
@@ -932,7 +920,14 @@ $tagFontSize: 12px;
} }
& div, & div,
span { span {
font-size: $tagFontSize;
font-size: 12px;
}
.tag-title-true {
display: grid;
grid-template-columns: 35% 35% 30%;
.first {
padding-left: 12px;
}
} }
.tag-title-false { .tag-title-false {
display: grid; display: grid;
@@ -960,11 +955,15 @@ $tagFontSize: 12px;
background-color: rgba(0, 0, 0, 0) !important; background-color: rgba(0, 0, 0, 0) !important;
} }
.more-action { .more-action {
color: #409eff;
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
} }
.tag-content-item-true {
display: grid;
grid-template-columns: 35% 35% 30%;
align-items: center;
}
.tag-content-item-false { .tag-content-item-false {
display: grid; display: grid;
grid-template-columns: 20% 40% 40%; grid-template-columns: 20% 40% 40%;
@@ -995,71 +994,13 @@ $tagFontSize: 12px;
} }
} }
.cl-saliency-map-pagination { .cl-saliency-map-pagination {
padding: $paginationPadding;
height: $paginationHeight;
padding: 0 32px;
height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
} }
} }
.cl-saliency-map-dialog {
.dialog-text {
display: flex;
.text {
line-height: 22px;
}
.dot {
margin: 7px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #00a5a7;
}
}
.dialog-label {
.label-item {
font-size: 12px;
display: inline-block;
background-color: #f5fbfb;
border: #dcdfe6 1px solid;
border-radius: 3px;
padding: 4px 10px;
}
}
.dialog-grid-container {
width: 620px;
height: 620px;
.dialog-loading {
width: 100%;
height: 100%;
}
.dialog-grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(3, 200px);
grid-template-rows: repeat(3, 200px);
gap: 10px;
.grid-item {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
& img {
object-fit: cover;
width: 90%;
height: 90%;
}
}
.grid-center {
background-color: rgba(00, 165, 167, 1);
grid-column: 2;
grid-row: 2;
}
}
}
}
.detail-container { .detail-container {
display: flex; display: flex;
align-items: center; align-items: center;


+ 296
- 133
mindinsight/ui/src/views/explain/xai-metric.vue View File

@@ -75,51 +75,68 @@ limitations under the License.
</el-tabs> </el-tabs>


<div class="cl-xai-con comprehensiveEvaluation" <div class="cl-xai-con comprehensiveEvaluation"
ref="xaiCon"
v-show="tabType==='overall' && !isNoData"> v-show="tabType==='overall' && !isNoData">
<el-table :data="evaluationTableData"
border
header-row-class-name="evaluation-table-header"
show-summary
:summary-method="getSummaries"
:sum-text="$t('metric.compositeScore')"
ref="sortTable">
<el-table-column prop="metric"
:label="$t('metric.metric')"
width="180"
class-name="firstColumn"
fixed
:resizable="false">
</el-table-column>
<el-table-column prop="weight"
:label="$t('metric.weightAllocatgion')"
width="180"
:class-name="!resultShow ? 'resultFalse':''"
fixed
:resizable="false">
<template slot-scope="scope">
<el-input-number v-model="scope.row.weight"
controls-position="right"
@change="weightChange"
:min="0"
:max="1"
:precision="2"
size="small"
:step="0.01"></el-input-number>
</template>
</el-table-column>
<el-table-column v-for="(item,index) in tableHead"
:key="index"
:prop="item"
sortable
min-width="120"
:resizable="false">
<template slot="header">
<div :title="item"
class="thTooltip">{{item}}</div>
</template>
</el-table-column>
</el-table>
<div class="cl-xai-con-flex">
<div class="cl-xai-con-flex-item">
<el-table :data="evaluationTableData"
border
header-row-class-name="evaluation-table-header"
show-summary
:summary-method="getSummaries"
:sum-text="$t('metric.compositeScore')"
ref="sortTable"
:max-height="maxHeight"
@cell-mouse-enter="cellHover"
@cell-mouse-leave="cellLeave">
<el-table-column prop="metric"
:label="$t('metric.metric')"
width="180"
class-name="firstColumn"
fixed
:resizable="false">
</el-table-column>
<el-table-column prop="weight"
:label="$t('metric.weightAllocatgion')"
width="180"
:class-name="!resultShow ? 'resultFalse':''"
fixed
:resizable="false">
<template slot-scope="scope">
<div @mouseenter="numberFocus(scope)"
@mouseleave="numberBlur(scope,$event)"
class="input-container">
<el-input-number v-model="scope.row.weight"
controls-position="right"
@change="weightChange()"
:min="0"
:max="1"
:precision="2"
size="small"
:step="0.01"
:controls="scope.row.controls"></el-input-number>
</div>
</template>
</el-table-column>
<el-table-column v-for="(item,index) in tableHead"
:key="index"
:prop="item"
sortable
min-width="120"
:resizable="false"
:class-name="property===item?'columnHover':'columnNoHover'">
<template slot="header">
<div :title="item"
class="thTooltip">{{item}}</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="cl-xai-con-flex-chart"
v-if="evaluationTableData.length >= minimumDimension">
<RadarChart :data="propChartData"
:nowHoverName="property"></RadarChart>
</div>
</div>
</div> </div>
<div class="cl-xai-con" <div class="cl-xai-con"
v-show="tabType==='detail' && !isNoData"> v-show="tabType==='detail' && !isNoData">
@@ -198,7 +215,8 @@ limitations under the License.
</div> </div>
<!-- No data --> <!-- No data -->
<div class="cl-xai-con" <div class="cl-xai-con"
v-show="isNoData">
v-show="isNoData"
ref="xaiCon">
<div class="image-noData"> <div class="image-noData">
<div> <div>
<img :src="require('@/assets/images/nodata.png')" <img :src="require('@/assets/images/nodata.png')"
@@ -215,14 +233,16 @@ limitations under the License.
</template> </template>


<script> <script>
import BenchmarkBarChart from '@/components/benchmarkBarChart';
import BenchmarkBarChart from '@/components/benchmark-bar-chart';
import RequestService from '../../services/request-service'; import RequestService from '../../services/request-service';
import SelectGroup from '../../components/selectGroup';
import SelectGroup from '../../components/select-group';
import RadarChart from '@/components/radar-chart';


export default { export default {
components: { components: {
BenchmarkBarChart, BenchmarkBarChart,
SelectGroup, SelectGroup,
RadarChart,
}, },
data() { data() {
return { return {
@@ -234,6 +254,10 @@ export default {
resultShow: true, // Result show resultShow: true, // Result show
isNoData: true, // Is no data isNoData: true, // Is no data
initOver: false, // Initialization completion flag initOver: false, // Initialization completion flag
maxHeight: 'auto', // Table max height
isChange: false, // Value is change
propChartData: [], // Prop chart data
property: '', // Table current property
fullDict: {}, // Full data dictionary fullDict: {}, // Full data dictionary
classifyAllMethods: [], // all explain methods classifyAllMethods: [], // all explain methods
// The currently selected exlpain method and all selected metrics // The currently selected exlpain method and all selected metrics
@@ -263,6 +287,7 @@ export default {
allLabels: [], // all labels allLabels: [], // all labels
resizeFlag: false, // Chart drawing area change sign resizeFlag: false, // Chart drawing area change sign
resizeTimer: null, // Chart redraw delay indicator resizeTimer: null, // Chart redraw delay indicator
minimumDimension: 3,
}; };
}, },
watch: { watch: {
@@ -292,11 +317,62 @@ export default {
document.title = `${decodeURIComponent(this.$route.query.id)}-${this.$t( document.title = `${decodeURIComponent(this.$route.query.id)}-${this.$t(
'metric.scoreSystem', 'metric.scoreSystem',
)}-MindInsight`; )}-MindInsight`;
this.maxHeight = this.$refs.xaiCon.clientHeight;
this.getEvaluationData(); this.getEvaluationData();
window.addEventListener('resize', this.resizeCallback, false); window.addEventListener('resize', this.resizeCallback, false);
}, },


methods: { methods: {
/**
* Table cell hover
* @param {Object} row Table row
* @param {Object} column Table column
*/
cellHover(row, column) {
this.property = column.property;
},
/**
* The logic execute when table cell mouse leave
*/
cellLeave() {
this.property = null;
},

/**
* Input number focus
* @param {Object} scope Table row scope
*/
numberFocus(scope) {
const tableData = this.$refs.sortTable.tableData;
tableData.forEach((item, index) => {
if (index === scope.$index) {
item.controls = true;
} else {
item.controls = false;
}
});
},

/**
* Input number blur
* @param {Object} scope Table row scope
* @param {Object} event event
*/
numberBlur(scope, event) {
const path = event.path || (event.composedPath && event.composedPath());
let noBlur = false;
path.some((item) => {
if (item.className && item.className.indexOf('el-input-number') > -1) {
return (noBlur = true);
}
});
if (noBlur) {
return;
}
scope.row.controls = false;
},

/** /**
* Jump back to train dashboard * Jump back to train dashboard
*/ */
@@ -334,12 +410,13 @@ export default {
score = 1 - score; score = 1 - score;
data.weight = score; data.weight = score;
} }
data.controls = false;
tableData.push(data); tableData.push(data);
}); });


const tableHead = []; const tableHead = [];
Object.keys(tableData[0]).forEach((item) => { Object.keys(tableData[0]).forEach((item) => {
if (item !== 'metric' && item !== 'weight') {
if (item !== 'metric' && item !== 'weight' && item !== 'controls') {
tableHead.push(item); tableHead.push(item);
} }
}); });
@@ -352,6 +429,7 @@ export default {
* Weight change * Weight change
*/ */
weightChange() { weightChange() {
this.isChange = true;
this.getSummaries(this.tableParam); this.getSummaries(this.tableParam);
}, },


@@ -377,7 +455,7 @@ export default {
} else if (index === 1) { } else if (index === 1) {
sums[index] = 0; sums[index] = 0;
values.forEach((value) => { values.forEach((value) => {
sums[index] += value;
sums[index] = this.floatAdd(sums[index], value);
}); });


if (sums[index] !== 1) { if (sums[index] !== 1) {
@@ -403,6 +481,30 @@ export default {
return sums; return sums;
}, },


/**
* Float add
* @param {Number} arg1 arg1
* @param {Number} arg2 arg2
* @return {Number}
*/
floatAdd(arg1, arg2) {
let r1;
let r2;
let m=0;
try {
r1=arg1.toString().split('.')[1].length;
} catch (e) {
r1=0;
}
try {
r2=arg2.toString().split('.')[1].length;
} catch (e) {
r2=0;
}
m=Math.pow(10, Math.max(r1, r2));
return (arg1*m+arg2*m)/m;
},

/** /**
* Get evaluation data * Get evaluation data
*/ */
@@ -416,11 +518,13 @@ export default {
if (res && res.data) { if (res && res.data) {
const resData = JSON.parse(JSON.stringify(res.data)); const resData = JSON.parse(JSON.stringify(res.data));
if ( if (
resData.explainer_scores.length &&
resData.explainer_scores &&
resData.explainer_scores.length &&
resData.explainer_scores[0].evaluations.length resData.explainer_scores[0].evaluations.length
) { ) {
this.isNoData = false; this.isNoData = false;
this.initEvaluationTable(resData); this.initEvaluationTable(resData);
this.processRadarData(resData);
this.initializeClassifyData(resData); this.initializeClassifyData(resData);
} else { } else {
this.isNoData = true; this.isNoData = true;
@@ -433,6 +537,36 @@ export default {
); );
}, },


/**
* Init evaluation table
* @param {Object} originalData Original data
*/
processRadarData(originalData) {
const data = {
legend: [],
indicator: [],
series: [],
};
const oriData = originalData.explainer_scores;
for (let i = 0; i < oriData.length; i++) {
data.legend.push(oriData[i].explainer);
data.series.push({
value: [],
name: oriData[i].explainer,
});
for (let j = 0; j < oriData[i].evaluations.length; j++) {
data.series[i].value.push(oriData[i].evaluations[j].score);
if (i === 0) {
data.indicator.push({
name: oriData[i].evaluations[j].metric,
});
}
}
}
data.title=this.$t('metric.radarChart');
this.propChartData = data;
},

/** /**
* Initialize classification evaluation data * Initialize classification evaluation data
* @param {Object} oriData Original data * @param {Object} oriData Original data
@@ -663,6 +797,11 @@ export default {
width: 0px; width: 0px;
height: 0px; height: 0px;
} }
.el-table__footer {
tr td:nth-child(2) {
text-align: center;
}
}
.el-tabs__item { .el-tabs__item {
font-size: 14px; font-size: 14px;
color: #303133; color: #303133;
@@ -706,6 +845,27 @@ export default {


.cl-xai-con { .cl-xai-con {
height: calc(100% - 95px); height: calc(100% - 95px);
.cl-xai-con-flex {
height: 100%;
display: flex;
}

.cl-xai-con-flex-item {
flex: 1;
overflow: hidden;
.input-container {
.el-input-number {
width: 100%;
}
}
}

.cl-xai-con-flex-chart {
width: 400px;
flex-shrink: 0;
border: 1px solid #e6ebf5;
margin-left: 20px;
}
} }


.comprehensiveEvaluation { .comprehensiveEvaluation {
@@ -730,104 +890,107 @@ export default {
background: #f5f7fa; background: #f5f7fa;
} }
} }
}

.image-noData {
// Set the width and white on the right.
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.noData-text {
margin-top: 33px;
font-size: 18px;
}
}


.classify-container {
height: 100%;
td.columnHover {
background: rgba(0, 165, 167, 0.05);
}


.left-item {
padding-right: 10px;
}
.right-itemm {
padding-left: 10px;
td.columnNoHover {
background: none;
}
} }
.half-item {
width: 50%;
float: left;

.image-noData {
// Set the width and white on the right.
width: 100%;
height: 100%; height: 100%;
overflow: hidden;
display: flex; display: flex;
justify-content: center;
align-items: center;
flex-direction: column; flex-direction: column;
.operate-container {
width: 100%;
.container-name {
font-size: 16px;
font-weight: 700;
padding: 15px 0px;
}
.select-see {
padding-bottom: 10px;
}
.see-methods {
font-size: 13px;
// font-weight: bold;
}
.select-name {
display: inline-block;
padding-right: 10px;
width: 100px;
}
.noData-text {
margin-top: 33px;
font-size: 18px;
} }
.chart-container {
flex: 1;
overflow: hidden;
}

.classify-container {
height: 100%;

.left-item {
padding-right: 10px;
}
.right-itemm {
padding-left: 10px;
} }
.methods-show {
padding: 10px 0px;
.show-name {
display: inline-block;
margin-right: 10px;
padding-bottom: 10px;
.half-item {
width: 50%;
float: left;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
.operate-container {
width: 100%;
.container-name {
font-size: 16px;
font-weight: 700;
padding: 15px 0px;
}
.select-see {
padding-bottom: 10px;
}
.see-methods {
font-size: 13px;
// font-weight: bold;
}
.select-name {
display: inline-block;
padding-right: 10px;
}
}
.chart-container {
flex: 1;
overflow: hidden;
}
.methods-show {
padding: 10px 0px;
} }
} }
} }
}


.classify {
border-right: solid 1px #ddd;
}
.slectMethod {
color: darkmagenta;
}
.classify {
border-right: solid 1px #ddd;
}
.slectMethod {
color: darkmagenta;
}


.el-select {
height: 32px;
width: 217px;
}
.el-input__inner {
height: 32px;
line-height: 32px;
padding: 0px 15px;
.el-select {
height: 32px;
width: 217px;
}
.el-input__inner {
height: 32px;
line-height: 32px;
padding: 0px 15px;
}
} }
}
.el-tooltip__popper {
.tooltip-container {
word-break: normal;
.tooltip-style {
.tooltip-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.tooltip-content {
line-height: 20px;
word-break: normal;
.el-tooltip__popper {
.tooltip-container {
word-break: normal;
.tooltip-style {
.tooltip-title {
font-size: 16px;
font-weight: bold;
color: #333333;
}
.tooltip-content {
line-height: 20px;
word-break: normal;
}
} }
} }
} }
}
</style> </style>

Loading…
Cancel
Save